Skip to content
Open
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
4 changes: 4 additions & 0 deletions .claude/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,10 @@ bun test
- 必须包含:`config.ts`, `src/index.ts`, `index.ts`
- 可选包含:`test/index.test.ts`, `DESIGN.md`

### 4. 认证处理规范
- 优先使用传入的 `accessToken`
- 未提供时,通过 `appId` 和 `appSecret` 自动获取
- 使用 `lib/auth.js` 中封装的认证函数
## 最佳实践

### 1. 代码兼容性
Expand Down
2 changes: 0 additions & 2 deletions lib/worker/loadTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,6 @@ const LoadToolsDev = async (filename: string): Promise<ToolType[]> => {
versionList: [],
version: toolSetVersion
});

tools.push(...children);
} else {
// is not toolset
Expand All @@ -80,7 +79,6 @@ const LoadToolsDev = async (filename: string): Promise<ToolType[]> => {
const toolVersion = (rootMod as any).versionList
? generateToolVersion((rootMod as any).versionList)
: generateToolVersion([]);

tools.push({
...(rootMod as ToolType),
tags: rootMod.tags || [ToolTagEnum.enum.other],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { defineTool } from '@tool/type';
import { FlowNodeInputTypeEnum, WorkflowIOValueTypeEnum } from '@tool/type/fastgpt';

export default defineTool({
name: {
'zh-CN': '获取草稿箱文章列表',
en: 'Get Draft List'
},
description: {
'zh-CN': '获取微信公众号草稿箱中的文章列表,支持分页查询',
en: 'Get the list of draft articles in WeChat Official Account draft box with pagination support'
},
toolDescription:
'获取微信公众号草稿箱中的文章列表。支持分页查询,可设置偏移量和每页数量。返回的草稿信息包括标题、作者、摘要、封面图等基本信息,可选择是否返回完整的文章内容。',
versionList: [
{
value: '0.1.0',
description: 'Default version',
inputs: [
{
key: 'accessToken',
label: '访问令牌',
description: '微信公众号 API 访问令牌(可选,与 appId/secret 二选一)',
renderTypeList: [FlowNodeInputTypeEnum.input, FlowNodeInputTypeEnum.reference],
valueType: WorkflowIOValueTypeEnum.string,
required: false
},
{
key: 'appId',
label: 'AppID',
description: '微信公众号 AppID(与 secret 配合使用,或使用 accessToken)',
renderTypeList: [FlowNodeInputTypeEnum.input, FlowNodeInputTypeEnum.reference],
valueType: WorkflowIOValueTypeEnum.string,
required: false
},
{
key: 'secret',
label: 'AppSecret',
description: '微信公众号 AppSecret(与 appId 配合使用,或使用 accessToken)',
renderTypeList: [FlowNodeInputTypeEnum.input, FlowNodeInputTypeEnum.reference],
valueType: WorkflowIOValueTypeEnum.string,
required: false
},
{
key: 'offset',
label: '偏移量',
description: '从全部素材的该偏移位置开始返回,0 表示从第一个素材返回,默认为 0',
renderTypeList: [FlowNodeInputTypeEnum.input, FlowNodeInputTypeEnum.reference],
valueType: WorkflowIOValueTypeEnum.number,
required: false,
toolDescription: 'offset for pagination, 0 means start from the first item'
},
{
key: 'count',
label: '返回数量',
description: '返回素材的数量,取值范围在 1 到 20 之间,默认为 20',
renderTypeList: [FlowNodeInputTypeEnum.input, FlowNodeInputTypeEnum.reference],
valueType: WorkflowIOValueTypeEnum.number,
required: false,
toolDescription: 'number of items to return, between 1 and 20'
},
{
key: 'noContent',
label: '不返回内容',
description: '是否不返回文章内容字段,1 表示不返回,0 表示返回,默认为 0',
renderTypeList: [FlowNodeInputTypeEnum.input, FlowNodeInputTypeEnum.reference],
valueType: WorkflowIOValueTypeEnum.number,
required: false,
toolDescription: '1 means no content field returned, 0 means content field returned'
}
],
outputs: [
{
valueType: WorkflowIOValueTypeEnum.number,
key: 'total_count',
label: '草稿总数',
description: '草稿箱中的草稿总数量'
},
{
valueType: WorkflowIOValueTypeEnum.number,
key: 'item_count',
label: '本次返回数量',
description: '本次返回的草稿数量'
},
{
valueType: WorkflowIOValueTypeEnum.object,
key: 'item',
label: '草稿列表',
description: '草稿文章列表数组,每个元素包含 media_id 和文章信息'
},
{
valueType: WorkflowIOValueTypeEnum.string,
key: 'error_message',
label: '错误信息',
description: '处理过程中的错误信息'
}
]
}
]
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import config from './config';
import { InputType, OutputType, tool as toolCb } from './src';
import { exportTool } from '@tool/utils/tool';

export default exportTool({
toolCb,
InputType,
OutputType,
config
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { z } from 'zod';
import { handleGetAuthToken, handleBatchGetDraft } from '../../../lib/handler';
import { addLog } from '@/utils/log';

export const InputType = z
.object({
// 认证参数(二选一)
accessToken: z.string().optional(),
appId: z.string().optional(),
secret: z.string().optional(),

// 查询参数
offset: z.number().int().min(0).optional().default(0),
count: z.number().int().min(1).max(20).optional().default(20),
noContent: z.number().int().min(0).max(1).optional()
})
.refine(
(data) => {
// 验证认证参数:要么提供 accessToken,要么同时提供 appId 和 secret
return data.accessToken || (data.appId && data.secret);
},
{
message: '必须提供 accessToken,或者同时提供 appId 和 secret',
path: ['认证参数']
}
);

export const OutputType = z.object({
total_count: z.number().optional(),
item_count: z.number().optional(),
item: z.array(z.any()).optional(),
error_message: z.string().optional()
});

export async function tool({
accessToken,
appId,
secret,
offset = 0,
count = 20,
noContent
}: z.infer<typeof InputType>): Promise<z.infer<typeof OutputType>> {
// 1. 获取 access_token
let token = accessToken;
if (!token) {
const result = await handleGetAuthToken({
grant_type: 'client_credential',
appid: appId!,
secret: secret!
});

if ('access_token' in result && result.access_token) {
token = result.access_token;
} else {
const errorMsg = (result as any).errmsg || '未知错误';
return {
error_message: `获取 access_token 失败: ${errorMsg} (错误码: ${(result as any).errcode})`
};
}
}

// 2. 获取草稿列表
const params: {
access_token: string;
offset: number;
count: number;
no_content?: number;
} = {
access_token: token,
offset,
count
};

if (noContent !== undefined) {
params.no_content = noContent;
}

const result = await handleBatchGetDraft(params);

if ('errcode' in result && result.errcode !== 0) {
return {
error_message: `获取草稿列表失败: ${result.errmsg} (错误码: ${result.errcode})`
};
}

addLog.info(
`Successfully retrieved ${result.item_count} drafts out of ${result.total_count} total`
);

return {
total_count: result.total_count,
item_count: result.item_count,
item: result.item
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,9 @@ async function processSingleArticle({
for (const imageUrl of imageUrls) {
try {
const wechatImageUrl = await uploadImageToWeChat(token, imageUrl);
processedHtml = processedHtml.replace(imageUrl, wechatImageUrl);
// 使用正则表达式全局替换,确保同一图片 URL 的所有出现都被替换
const escapedUrl = imageUrl.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
processedHtml = processedHtml.replace(new RegExp(escapedUrl, 'g'), wechatImageUrl);
} catch (error) {
console.warn(`上传图片失败: ${imageUrl}`, error);
// 保持原链接,继续处理其他图片
Expand Down
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@
"dependencies": {
"@ts-rest/core": "3.52.1",
"@ts-rest/express": "3.52.1",
"@ts-rest/open-api": "3.52.1"
"@ts-rest/open-api": "3.52.1",
"@types/cheerio": "^0.22.35",
"@types/marked": "^5.0.2",
"cheerio": "^1.1.0",
"marked": "^17.0.1"
}
}
Loading