Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 63 additions & 1 deletion .claude/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,63 @@ bun run start
- 验证构建后的代码在 `dist` 目录中正确生成
- 测试生产环境的启动和运行

## 工具开发规范

### 1. 错误处理规范
**⚠️ 重要**: 工具函数内部不需要进行顶层的 try-catch,直接把错误抛出到外面处理

```typescript
// ✅ 正确:直接抛出错误
export async function tool(input: ToolInput): Promise<ToolOutput> {
// 1. 获取 access_token
const result = await handleGetAuthToken({
grant_type: 'client_credential',
appid: input.appId!,
secret: input.appSecret!
});

if ('errcode' in result && result.errcode !== 0) {
return {
error_message: `获取 access_token 失败: ${result.errmsg}`
};
}

// 直接执行操作,让错误自然抛出
const processedData = await processData(result.access_token);
return processedData;
}

// ❌ 错误:顶层 try-catch
export async function tool(input: ToolInput): Promise<ToolOutput> {
try {
// 业务逻辑
const result = await someOperation();
return result;
} catch (error) {
// 不要在这里处理所有错误
return {
error_message: error.message
};
}
}
```

### 2. 测试规范
**⚠️ 重要**: 测试应该使用 `bun run test` 而不是 `bun test`

```bash
# ✅ 正确的测试命令
bun run test

# ❌ 错误的测试命令
bun test
```

### 3. 工具结构规范
- 每个工具都应该有自己的目录:`children/toolName/`
- 必须包含:`config.ts`, `src/index.ts`, `index.ts`
- 可选包含:`test/index.test.ts`, `DESIGN.md`

## 最佳实践

### 1. 代码兼容性
Expand All @@ -229,6 +286,11 @@ bun run start
- 执行完整的测试套件
- 验证跨环境兼容性

### 4. 错误处理
- 工具函数内部避免顶层 try-catch
- 让错误自然抛出,由外部处理
- 对于已知的业务错误,返回结构化的错误信息

## 常见问题

### Q: 如何确保代码在两个环境都兼容?
Expand All @@ -241,4 +303,4 @@ A: 使用 git sparse checkout,配置只需要的目录和文件。
A: 检查 `dist` 目录结构,确保所有依赖都正确安装,验证 Node.js 版本兼容性。

### Q: 如何调试生产环境问题?
A: 在 `dist` 目录中设置断点,使用 Node.js 调试工具,检查构建日志。
A: 在 `dist` 目录中设置断点,使用 Node.js 调试工具,检查构建日志。
4 changes: 4 additions & 0 deletions lib/s3/const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,7 @@ export const mimeMap: Record<string, string> = {
'.js': 'application/javascript',
'.md': 'text/markdown'
};

export const PublicBucketBaseURL = process.env.S3_EXTERNAL_BASE_URL
? `${process.env.S3_EXTERNAL_BASE_URL}/${process.env.S3_PUBLIC_BUCKET}`
: `${process.env.S3_USE_SSL ? 'https' : 'http'}://${process.env.S3_ENDPOINT}/${process.env.S3_PUBLIC_BUCKET}`;
15 changes: 13 additions & 2 deletions lib/type/env.d.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,24 @@
declare namespace NodeJS {
interface ProcessEnv {
PORT: string;
AUTH_TOKEN: string;
LOG_LEVEL: string;
MODEL_PROVIDER_PRIORITY: string;
SIGNOZ_BASE_URL: string;
SIGNOZ_SERVICE_NAME: string;
MONGODB_URI: string;
REDIS_URL: string;
SERVICE_REQUEST_MAX_CONTENT_LENGTH: string;
MAX_API_SIZE: string;
DISABLE_DEV_TOOLS: string;
S3_PRIVATE_BUCKET: string;
S3_PUBLIC_BUCKET: string;
S3_EXTERNAL_BASE_URL: string;
S3_ENDPOINT: string;
S3_PORT: string;
S3_USE_SSL: string;
S3_ACCESS_KEY: string;
S3_SECRET_KEY: string;
S3_BUCKET: string;
MAX_FILE_SIZE: string;
RETENTION_DAYS: string;
}
}
82 changes: 82 additions & 0 deletions lib/worker/loadTool.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { isProd } from '@/constants';
import { addLog } from '@/utils/log';
import { basePath, devToolIds } from '@tool/constants';
import { LoadToolsByFilename } from '@tool/loadToolProd';
import { getIconPath } from '@tool/parseMod';
import type { ToolSetType, ToolType } from '@tool/type';
import { ToolTagEnum } from '@tool/type/tags';
import { existsSync } from 'fs';
import { readdir } from 'fs/promises';
import { join } from 'path';

const LoadToolsDev = async (filename: string): Promise<ToolType[]> => {
if (isProd) {
addLog.error('Can not load dev tool in prod mode');
return [];
}

const tools: ToolType[] = [];

const toolPath = join(basePath, 'modules', 'tool', 'packages', filename);

const rootMod = (await import(toolPath)).default as ToolSetType | ToolType;

const childrenPath = join(toolPath, 'children');
const isToolSet = existsSync(childrenPath);

const toolsetId = rootMod.toolId || filename;
const parentIcon = rootMod.icon ?? getIconPath(`${toolsetId}/logo`);

if (isToolSet) {
tools.push({
...rootMod,
tags: rootMod.tags || [ToolTagEnum.enum.other],
toolId: toolsetId,
icon: parentIcon,
toolFilename: filename,
cb: () => Promise.resolve({}),
versionList: []
});

const children: ToolType[] = [];

{
const files = await readdir(childrenPath);
for (const file of files) {
const childPath = join(childrenPath, file);

const childMod = (await import(childPath)).default as ToolType;
const toolId = childMod.toolId || `${toolsetId}/${file}`;

const childIcon = childMod.icon ?? rootMod.icon ?? getIconPath(`${toolsetId}/${file}/logo`);
children.push({
...childMod,
toolId,
toolFilename: filename,
icon: childIcon,
parentId: toolsetId
});
}
}

tools.push(...children);
} else {
// is not toolset
const icon = rootMod.icon ?? getIconPath(`${toolsetId}/logo`);

tools.push({
...(rootMod as ToolType),
tags: rootMod.tags || [ToolTagEnum.enum.other],
toolId: toolsetId,
icon,
toolFilename: filename
});
}

tools.forEach((tool) => devToolIds.add(tool.toolId));
return tools;
};

export const loadTool = async (filename: string, dev: boolean) => {
return dev ? await LoadToolsDev(filename) : await LoadToolsByFilename(filename);
};
11 changes: 4 additions & 7 deletions lib/worker/worker.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { parentPort } from 'worker_threads';
import type { Main2WorkerMessageType } from './type';
import { setupProxy } from '../utils/setupProxy';
import { LoadToolsByFilename } from '@tool/utils';
import { getErrText } from '@tool/utils/err';
import { LoadToolsDev } from '@tool/loadToolDev';
import type { ToolCallbackReturnSchemaType } from '@tool/type/req';
import { loadTool } from './loadTool';

setupProxy();

Expand All @@ -20,11 +19,9 @@ parentPort?.on('message', async (params: Main2WorkerMessageType) => {
const { type, data } = params;
switch (type) {
case 'runTool': {
const tools = data.dev
? await LoadToolsDev(data.filename)
: await LoadToolsByFilename(data.filename);

const tool = tools.find((tool) => tool.toolId === data.toolId);
const tool = (await loadTool(data.filename, data.dev)).find(
(tool) => tool.toolId === data.toolId
);

if (!tool || !tool.cb) {
parentPort?.postMessage({
Expand Down
2 changes: 1 addition & 1 deletion modules/tool/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ import { refreshDir } from '@/utils/fs';
import { addLog } from '@/utils/log';
import { basePath, toolsDir, UploadToolsS3Path } from './constants';
import { privateS3Server } from '@/s3';
import { LoadToolsByFilename } from './utils';
import { stat } from 'fs/promises';
import { getCachedData } from '@/cache';
import { SystemCacheKeyEnum } from '@/cache/type';
import { batch } from '@/utils/parallel';
import { LoadToolsByFilename } from './loadToolProd';

const filterToolList = ['.DS_Store', '.git', '.github', 'node_modules', 'dist', 'scripts'];

Expand Down
17 changes: 17 additions & 0 deletions modules/tool/loadToolProd.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { toolsDir } from './constants';
import type { ToolSetType, ToolType } from './type';
import { addLog } from '@/utils/log';
import { join } from 'path';
import { parseMod } from './parseMod';

// Load tool or toolset and its children
export const LoadToolsByFilename = async (filename: string): Promise<ToolType[]> => {
const rootMod = (await import(join(toolsDir, filename))).default as ToolType | ToolSetType;

if (!rootMod.toolId) {
addLog.error(`Can not parse toolId, filename: ${filename}`);
return [];
}

return parseMod({ rootMod, filename });
};
66 changes: 66 additions & 0 deletions modules/tool/packages/wechatOfficialAccount/DESIGN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# 微信公众号工具集

### 项目结构

```
wechatOfficialAccount/
├── children/
│ └── getAuthToken/ # 获取微信公众号鉴权信息子工具
│ ├── config.ts # 工具配置文件
│ ├── src/
│ │ └── index.ts # 工具核心逻辑实现
│ └── test/
│ └── index.test.ts # 测试文件
├── lib/
│ ├── api.ts # 微信公众号 API 定义
│ └── auth.ts # 通用 API 处理器
├── assets/ # 静态资源
├── lib/ # 构建输出目录
├── config.ts # 工具集配置
├── index.ts # 工具集入口文件
├── package.json # 包配置
├── DESIGN.md # 设计文档
└── README.md # 使用说明
```

### 工具集/子工具列表

#### 1. 获取微信公众号鉴权信息 (getAuthToken)
- **功能**: 通过 AppID 和 AppSecret 获取微信公众号的 access_token
- **API**: `GET https://api.weixin.qq.com/cgi-bin/token`
- **输入**: AppID, AppSecret
- **输出**: access_token, expires_in

#### 2. 上传素材 (uploadImage)
- **功能**: 上传图片素材到微信公众号素材库
- **API**: `POST https://api.weixin.qq.com/cgi-bin/media/uploadimg`
- **输入**: access_token, 图片文件
- **输出**: 图片URL

#### 3. 获取素材 media_id (getMaterial)
- **功能**: 根据媒体ID获取素材内容
- **API**: `POST https://api.weixin.qq.com/cgi-bin/material/get_material`
- **输入**: access_token, media_id
- **输出**: 素材内容

#### 4. 发布 markdown 格式的内容到草稿箱 (addDraft)
- **功能**: 将 markdown 格式内容转换为图文素材并添加到草稿箱
- **API**: `POST https://api.weixin.qq.com/cgi-bin/draft/add`
- **输入**: access_token, 文章列表
- **输出**: media_id

#### 5. 获取草稿箱中的内容列表 (batchGetDraft)
- **功能**: 获取草稿箱中的图文列表
- **API**: `POST https://api.weixin.qq.com/cgi-bin/draft/batchget`
- **输入**: access_token, offset, count
- **输出**: 草稿列表

#### 6. 发布草稿箱中的内容 (submitPublish)
- **功能**: 将草稿箱中的内容发布
- **API**: `POST https://api.weixin.qq.com/cgi-bin/freepublish/submit`
- **输入**: access_token, media_id
- **输出**: publish_id, msg_data_id

---

下面由 AI 生成完整的设计文档
4 changes: 4 additions & 0 deletions modules/tool/packages/wechatOfficialAccount/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# 获取密钥

按照如图所示的方式获取密钥
![](./assets/get-secrets.jpg)
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading