Skip to content

Commit 1443607

Browse files
authored
feat: new tool wechat official account toolset; fix: worker (#289)
* feat: new tool wechat official account toolset; fix: worker * fix: claude
1 parent a0016d9 commit 1443607

File tree

36 files changed

+3002
-147
lines changed

36 files changed

+3002
-147
lines changed

.claude/CLAUDE.md

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,63 @@ bun run start
212212
- 验证构建后的代码在 `dist` 目录中正确生成
213213
- 测试生产环境的启动和运行
214214

215+
## 工具开发规范
216+
217+
### 1. 错误处理规范
218+
**⚠️ 重要**: 工具函数内部不需要进行顶层的 try-catch,直接把错误抛出到外面处理
219+
220+
```typescript
221+
// ✅ 正确:直接抛出错误
222+
export async function tool(input: ToolInput): Promise<ToolOutput> {
223+
// 1. 获取 access_token
224+
const result = await handleGetAuthToken({
225+
grant_type: 'client_credential',
226+
appid: input.appId!,
227+
secret: input.appSecret!
228+
});
229+
230+
if ('errcode' in result && result.errcode !== 0) {
231+
return {
232+
error_message: `获取 access_token 失败: ${result.errmsg}`
233+
};
234+
}
235+
236+
// 直接执行操作,让错误自然抛出
237+
const processedData = await processData(result.access_token);
238+
return processedData;
239+
}
240+
241+
// ❌ 错误:顶层 try-catch
242+
export async function tool(input: ToolInput): Promise<ToolOutput> {
243+
try {
244+
// 业务逻辑
245+
const result = await someOperation();
246+
return result;
247+
} catch (error) {
248+
// 不要在这里处理所有错误
249+
return {
250+
error_message: error.message
251+
};
252+
}
253+
}
254+
```
255+
256+
### 2. 测试规范
257+
**⚠️ 重要**: 测试应该使用 `bun run test` 而不是 `bun test`
258+
259+
```bash
260+
# ✅ 正确的测试命令
261+
bun run test
262+
263+
# ❌ 错误的测试命令
264+
bun test
265+
```
266+
267+
### 3. 工具结构规范
268+
- 每个工具都应该有自己的目录:`children/toolName/`
269+
- 必须包含:`config.ts`, `src/index.ts`, `index.ts`
270+
- 可选包含:`test/index.test.ts`, `DESIGN.md`
271+
215272
## 最佳实践
216273

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

289+
### 4. 错误处理
290+
- 工具函数内部避免顶层 try-catch
291+
- 让错误自然抛出,由外部处理
292+
- 对于已知的业务错误,返回结构化的错误信息
293+
232294
## 常见问题
233295

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

243305
### Q: 如何调试生产环境问题?
244-
A: 在 `dist` 目录中设置断点,使用 Node.js 调试工具,检查构建日志。
306+
A: 在 `dist` 目录中设置断点,使用 Node.js 调试工具,检查构建日志。

lib/s3/const.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,7 @@ export const mimeMap: Record<string, string> = {
1919
'.js': 'application/javascript',
2020
'.md': 'text/markdown'
2121
};
22+
23+
export const PublicBucketBaseURL = process.env.S3_EXTERNAL_BASE_URL
24+
? `${process.env.S3_EXTERNAL_BASE_URL}/${process.env.S3_PUBLIC_BUCKET}`
25+
: `${process.env.S3_USE_SSL ? 'https' : 'http'}://${process.env.S3_ENDPOINT}/${process.env.S3_PUBLIC_BUCKET}`;

lib/type/env.d.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,24 @@
11
declare namespace NodeJS {
22
interface ProcessEnv {
3+
PORT: string;
4+
AUTH_TOKEN: string;
5+
LOG_LEVEL: string;
6+
MODEL_PROVIDER_PRIORITY: string;
7+
SIGNOZ_BASE_URL: string;
8+
SIGNOZ_SERVICE_NAME: string;
9+
MONGODB_URI: string;
10+
REDIS_URL: string;
11+
SERVICE_REQUEST_MAX_CONTENT_LENGTH: string;
12+
MAX_API_SIZE: string;
13+
DISABLE_DEV_TOOLS: string;
14+
S3_PRIVATE_BUCKET: string;
15+
S3_PUBLIC_BUCKET: string;
316
S3_EXTERNAL_BASE_URL: string;
417
S3_ENDPOINT: string;
518
S3_PORT: string;
619
S3_USE_SSL: string;
720
S3_ACCESS_KEY: string;
821
S3_SECRET_KEY: string;
9-
S3_BUCKET: string;
1022
MAX_FILE_SIZE: string;
11-
RETENTION_DAYS: string;
1223
}
1324
}

lib/worker/loadTool.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { isProd } from '@/constants';
2+
import { addLog } from '@/utils/log';
3+
import { basePath, devToolIds } from '@tool/constants';
4+
import { LoadToolsByFilename } from '@tool/loadToolProd';
5+
import { getIconPath } from '@tool/parseMod';
6+
import type { ToolSetType, ToolType } from '@tool/type';
7+
import { ToolTagEnum } from '@tool/type/tags';
8+
import { existsSync } from 'fs';
9+
import { readdir } from 'fs/promises';
10+
import { join } from 'path';
11+
12+
const LoadToolsDev = async (filename: string): Promise<ToolType[]> => {
13+
if (isProd) {
14+
addLog.error('Can not load dev tool in prod mode');
15+
return [];
16+
}
17+
18+
const tools: ToolType[] = [];
19+
20+
const toolPath = join(basePath, 'modules', 'tool', 'packages', filename);
21+
22+
const rootMod = (await import(toolPath)).default as ToolSetType | ToolType;
23+
24+
const childrenPath = join(toolPath, 'children');
25+
const isToolSet = existsSync(childrenPath);
26+
27+
const toolsetId = rootMod.toolId || filename;
28+
const parentIcon = rootMod.icon ?? getIconPath(`${toolsetId}/logo`);
29+
30+
if (isToolSet) {
31+
tools.push({
32+
...rootMod,
33+
tags: rootMod.tags || [ToolTagEnum.enum.other],
34+
toolId: toolsetId,
35+
icon: parentIcon,
36+
toolFilename: filename,
37+
cb: () => Promise.resolve({}),
38+
versionList: []
39+
});
40+
41+
const children: ToolType[] = [];
42+
43+
{
44+
const files = await readdir(childrenPath);
45+
for (const file of files) {
46+
const childPath = join(childrenPath, file);
47+
48+
const childMod = (await import(childPath)).default as ToolType;
49+
const toolId = childMod.toolId || `${toolsetId}/${file}`;
50+
51+
const childIcon = childMod.icon ?? rootMod.icon ?? getIconPath(`${toolsetId}/${file}/logo`);
52+
children.push({
53+
...childMod,
54+
toolId,
55+
toolFilename: filename,
56+
icon: childIcon,
57+
parentId: toolsetId
58+
});
59+
}
60+
}
61+
62+
tools.push(...children);
63+
} else {
64+
// is not toolset
65+
const icon = rootMod.icon ?? getIconPath(`${toolsetId}/logo`);
66+
67+
tools.push({
68+
...(rootMod as ToolType),
69+
tags: rootMod.tags || [ToolTagEnum.enum.other],
70+
toolId: toolsetId,
71+
icon,
72+
toolFilename: filename
73+
});
74+
}
75+
76+
tools.forEach((tool) => devToolIds.add(tool.toolId));
77+
return tools;
78+
};
79+
80+
export const loadTool = async (filename: string, dev: boolean) => {
81+
return dev ? await LoadToolsDev(filename) : await LoadToolsByFilename(filename);
82+
};

lib/worker/worker.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
import { parentPort } from 'worker_threads';
22
import type { Main2WorkerMessageType } from './type';
33
import { setupProxy } from '../utils/setupProxy';
4-
import { LoadToolsByFilename } from '@tool/utils';
54
import { getErrText } from '@tool/utils/err';
6-
import { LoadToolsDev } from '@tool/loadToolDev';
75
import type { ToolCallbackReturnSchemaType } from '@tool/type/req';
6+
import { loadTool } from './loadTool';
87

98
setupProxy();
109

@@ -20,11 +19,9 @@ parentPort?.on('message', async (params: Main2WorkerMessageType) => {
2019
const { type, data } = params;
2120
switch (type) {
2221
case 'runTool': {
23-
const tools = data.dev
24-
? await LoadToolsDev(data.filename)
25-
: await LoadToolsByFilename(data.filename);
26-
27-
const tool = tools.find((tool) => tool.toolId === data.toolId);
22+
const tool = (await loadTool(data.filename, data.dev)).find(
23+
(tool) => tool.toolId === data.toolId
24+
);
2825

2926
if (!tool || !tool.cb) {
3027
parentPort?.postMessage({

modules/tool/init.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@ import { refreshDir } from '@/utils/fs';
88
import { addLog } from '@/utils/log';
99
import { basePath, toolsDir, UploadToolsS3Path } from './constants';
1010
import { privateS3Server } from '@/s3';
11-
import { LoadToolsByFilename } from './utils';
1211
import { stat } from 'fs/promises';
1312
import { getCachedData } from '@/cache';
1413
import { SystemCacheKeyEnum } from '@/cache/type';
1514
import { batch } from '@/utils/parallel';
15+
import { LoadToolsByFilename } from './loadToolProd';
1616

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

modules/tool/loadToolProd.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { toolsDir } from './constants';
2+
import type { ToolSetType, ToolType } from './type';
3+
import { addLog } from '@/utils/log';
4+
import { join } from 'path';
5+
import { parseMod } from './parseMod';
6+
7+
// Load tool or toolset and its children
8+
export const LoadToolsByFilename = async (filename: string): Promise<ToolType[]> => {
9+
const rootMod = (await import(join(toolsDir, filename))).default as ToolType | ToolSetType;
10+
11+
if (!rootMod.toolId) {
12+
addLog.error(`Can not parse toolId, filename: ${filename}`);
13+
return [];
14+
}
15+
16+
return parseMod({ rootMod, filename });
17+
};
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# 微信公众号工具集
2+
3+
### 项目结构
4+
5+
```
6+
wechatOfficialAccount/
7+
├── children/
8+
│ └── getAuthToken/ # 获取微信公众号鉴权信息子工具
9+
│ ├── config.ts # 工具配置文件
10+
│ ├── src/
11+
│ │ └── index.ts # 工具核心逻辑实现
12+
│ └── test/
13+
│ └── index.test.ts # 测试文件
14+
├── lib/
15+
│ ├── api.ts # 微信公众号 API 定义
16+
│ └── auth.ts # 通用 API 处理器
17+
├── assets/ # 静态资源
18+
├── lib/ # 构建输出目录
19+
├── config.ts # 工具集配置
20+
├── index.ts # 工具集入口文件
21+
├── package.json # 包配置
22+
├── DESIGN.md # 设计文档
23+
└── README.md # 使用说明
24+
```
25+
26+
### 工具集/子工具列表
27+
28+
#### 1. 获取微信公众号鉴权信息 (getAuthToken)
29+
- **功能**: 通过 AppID 和 AppSecret 获取微信公众号的 access_token
30+
- **API**: `GET https://api.weixin.qq.com/cgi-bin/token`
31+
- **输入**: AppID, AppSecret
32+
- **输出**: access_token, expires_in
33+
34+
#### 2. 上传素材 (uploadImage)
35+
- **功能**: 上传图片素材到微信公众号素材库
36+
- **API**: `POST https://api.weixin.qq.com/cgi-bin/media/uploadimg`
37+
- **输入**: access_token, 图片文件
38+
- **输出**: 图片URL
39+
40+
#### 3. 获取素材 media_id (getMaterial)
41+
- **功能**: 根据媒体ID获取素材内容
42+
- **API**: `POST https://api.weixin.qq.com/cgi-bin/material/get_material`
43+
- **输入**: access_token, media_id
44+
- **输出**: 素材内容
45+
46+
#### 4. 发布 markdown 格式的内容到草稿箱 (addDraft)
47+
- **功能**: 将 markdown 格式内容转换为图文素材并添加到草稿箱
48+
- **API**: `POST https://api.weixin.qq.com/cgi-bin/draft/add`
49+
- **输入**: access_token, 文章列表
50+
- **输出**: media_id
51+
52+
#### 5. 获取草稿箱中的内容列表 (batchGetDraft)
53+
- **功能**: 获取草稿箱中的图文列表
54+
- **API**: `POST https://api.weixin.qq.com/cgi-bin/draft/batchget`
55+
- **输入**: access_token, offset, count
56+
- **输出**: 草稿列表
57+
58+
#### 6. 发布草稿箱中的内容 (submitPublish)
59+
- **功能**: 将草稿箱中的内容发布
60+
- **API**: `POST https://api.weixin.qq.com/cgi-bin/freepublish/submit`
61+
- **输入**: access_token, media_id
62+
- **输出**: publish_id, msg_data_id
63+
64+
---
65+
66+
下面由 AI 生成完整的设计文档
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# 获取密钥
2+
3+
按照如图所示的方式获取密钥
4+
![](./assets/get-secrets.jpg)
523 KB
Loading

0 commit comments

Comments
 (0)