aiconductor-cli 1.5.1 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +565 -419
- package/bin/aiconductor-cli.js +401 -296
- package/package.json +1 -1
- package/src/api.js +361 -2
- package/src/api.test.js +553 -3
- package/src/commands/content.js +184 -184
- package/src/commands/douyin.js +174 -0
- package/src/commands/html_publish.js +54 -54
- package/src/commands/md_publish.js +175 -175
- package/src/commands/web.js +69 -0
- package/src/commands/web_search.js +99 -99
- package/src/commands/wx.js +116 -116
- package/src/commands/xhs.js +763 -268
- package/src/lib/cli-utils.js +172 -84
- package/src/lib/daren-score.js +91 -0
- package/src/lib/daren-score.test.js +57 -0
- package/AGENTS.md +0 -189
- package/api_docs/aic_web_search.md +0 -84
- package/api_docs/audio_converter.md +0 -51
- package/api_docs/balance_query.md +0 -38
- package/api_docs/bing_search.md +0 -55
- package/api_docs/china_stock_data.md +0 -221
- package/api_docs/delete_published_webpage.md +0 -38
- package/api_docs/doubao_search.md +0 -80
- package/api_docs/douyin_link_to_text_v1.md +0 -50
- package/api_docs/douyin_link_to_text_v2.md +0 -50
- package/api_docs/douyin_user_detail.md +0 -65
- package/api_docs/douyin_user_search.md +0 -58
- package/api_docs/douyin_user_videos.md +0 -49
- package/api_docs/douyin_user_videos_async.md +0 -45
- package/api_docs/douyin_user_videos_async_query.md +0 -41
- package/api_docs/douyin_user_videos_max.md +0 -59
- package/api_docs/douyin_video_comments.md +0 -68
- package/api_docs/douyin_video_info.md +0 -70
- package/api_docs/douyin_video_search.md +0 -60
- package/api_docs/douyin_video_search_fast.md +0 -48
- package/api_docs/fast_ocr.md +0 -51
- package/api_docs/flow_article2podcast_creat.md +0 -41
- package/api_docs/flow_article2podcast_query.md +0 -41
- package/api_docs/flow_wx_history_articles.md +0 -45
- package/api_docs/generate_image.md +0 -68
- package/api_docs/get_duration.md +0 -37
- package/api_docs/hot_list_mixed.md +0 -122
- package/api_docs/html_publish.md +0 -50
- package/api_docs/html_publish_flexible.md +0 -65
- package/api_docs/image_resize.md +0 -58
- package/api_docs/image_to_permalink.md +0 -61
- package/api_docs/link_expander.md +0 -39
- package/api_docs/llm_proxy.md +0 -61
- package/api_docs/markitdown.md +0 -47
- package/api_docs/md_to_docx.md +0 -51
- package/api_docs/md_to_html.md +0 -53
- package/api_docs/md_to_mindmap.md +0 -52
- package/api_docs/md_to_pdf.md +0 -52
- package/api_docs/md_to_png.md +0 -53
- package/api_docs/md_to_txt.md +0 -51
- package/api_docs/md_to_wxhtml.md +0 -64
- package/api_docs/md_to_xhs.md +0 -87
- package/api_docs/md_to_xlsx.md +0 -52
- package/api_docs/portrait_enhancement.md +0 -69
- package/api_docs/published_webpages.md +0 -45
- package/api_docs/r2_uploader.md +0 -51
- package/api_docs/schedulerx_job_create.md +0 -56
- package/api_docs/transaction_query.md +0 -52
- package/api_docs/url_to_markdown.md +0 -50
- package/api_docs/video_download_url.md +0 -57
- package/api_docs/video_download_url_oss.md +0 -55
- package/api_docs/video_to_text.md +0 -48
- package/api_docs/video_to_text_v2.md +0 -49
- package/api_docs/volcengine_face_pretty.md +0 -55
- package/api_docs/volcengine_faceswap.md +0 -55
- package/api_docs/website_screenshot.md +0 -49
- package/api_docs/wx_access_token.md +0 -44
- package/api_docs/wx_article_analysis_data_push.md +0 -43
- package/api_docs/wx_article_data.md +0 -48
- package/api_docs/wx_article_style_rewrite.md +0 -45
- package/api_docs/wx_draft_add.md +0 -56
- package/api_docs/wx_draft_publish.md +0 -47
- package/api_docs/wx_history_articles.md +0 -89
- package/api_docs/wx_image_upload.md +0 -46
- package/api_docs/wx_md_to_html.md +0 -48
- package/api_docs/wx_rss_reader.md +0 -59
- package/api_docs/xhs_hot_list.md +0 -48
- package/api_docs/xhs_note_comments_fast.md +0 -54
- package/api_docs/xhs_note_info_v2.md +0 -45
- package/api_docs/xhs_note_info_v3.md +0 -69
- package/api_docs/xhs_note_publish.md +0 -54
- package/api_docs/xhs_note_search_v2.md +0 -73
- package/api_docs/xhs_note_search_v3.md +0 -54
- package/api_docs/xhs_user_info_v2.md +0 -61
- package/api_docs/xhs_user_notes_v2.md +0 -56
- package/api_docs/xhs_user_notes_v3.md +0 -48
package/src/lib/cli-utils.js
CHANGED
|
@@ -1,84 +1,172 @@
|
|
|
1
|
-
const { resolveApiKey } = require("../api");
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* 统一成功输出
|
|
5
|
-
* @param {object} data - API 返回数据
|
|
6
|
-
* @param {"json"|"text"} format
|
|
7
|
-
* @param {function} [textFormatter] - text 模式的自定义格式化函数,接收 data 返回字符串
|
|
8
|
-
*/
|
|
9
|
-
function outputResult(data, format, textFormatter) {
|
|
10
|
-
if (format === "json") {
|
|
11
|
-
process.stdout.write(JSON.stringify({ success: true, data }, null, 2) + "\n");
|
|
12
|
-
} else {
|
|
13
|
-
const text = textFormatter ? textFormatter(data) : JSON.stringify(data, null, 2);
|
|
14
|
-
process.stdout.write(text + "\n");
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* 统一错误输出
|
|
20
|
-
* @param {object} err - 错误对象(需有 message 属性)
|
|
21
|
-
* @param {"json"|"text"} format
|
|
22
|
-
*/
|
|
23
|
-
function outputError(err, format) {
|
|
24
|
-
const msg = err.message || String(err);
|
|
25
|
-
if (format === "json") {
|
|
26
|
-
process.stderr.write(JSON.stringify({ success: false, error: msg }, null, 2) + "\n");
|
|
27
|
-
} else {
|
|
28
|
-
process.stderr.write(`✗ 错误: ${msg}\n`);
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* 解析 API Key,失败时输出错误并退出进程
|
|
34
|
-
* @param {string} [optionKey] - 命令行传入的 key
|
|
35
|
-
* @param {"json"|"text"} format
|
|
36
|
-
* @returns {string} 解析到的 API Key
|
|
37
|
-
*/
|
|
38
|
-
function resolveKeyOrExit(optionKey, format) {
|
|
39
|
-
const key = resolveApiKey(optionKey);
|
|
40
|
-
if (!key) {
|
|
41
|
-
outputError(
|
|
42
|
-
{ message: "缺少 API Key。请通过 -k 参数、AICONDUCTOR_API_KEY 环境变量或 .env 文件提供。" },
|
|
43
|
-
format
|
|
44
|
-
);
|
|
45
|
-
process.exit(1);
|
|
46
|
-
}
|
|
47
|
-
return key;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* 统一 API 错误处理(不退出,由调用方决定退出码)
|
|
52
|
-
* @param {object} err - 错误对象
|
|
53
|
-
* @param {"json"|"text"} format
|
|
54
|
-
*/
|
|
55
|
-
function handleApiError(err, format) {
|
|
56
|
-
if (err.type === "http_error") {
|
|
57
|
-
outputError(
|
|
58
|
-
{ message: `API 返回错误 (HTTP ${err.statusCode}): ${err.body?.message || JSON.stringify(err.body)}` },
|
|
59
|
-
format
|
|
60
|
-
);
|
|
61
|
-
} else if (err.type === "network_error") {
|
|
62
|
-
outputError({ message: `网络错误: ${err.message}` }, format);
|
|
63
|
-
} else if (err.type === "validation_error") {
|
|
64
|
-
outputError({ message: `参数错误: ${err.message}` }, format);
|
|
65
|
-
} else if (err.type === "api_error") {
|
|
66
|
-
outputError({ message: err.message }, format);
|
|
67
|
-
} else {
|
|
68
|
-
outputError({ message: `未知错误: ${err.message || err}` }, format);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* 安全解析整数,无效时返回 defaultValue
|
|
74
|
-
* @param {*} value
|
|
75
|
-
* @param {number} defaultValue
|
|
76
|
-
* @returns {number}
|
|
77
|
-
*/
|
|
78
|
-
function safeParseInt(value, defaultValue) {
|
|
79
|
-
if (value === undefined || value === null) return defaultValue;
|
|
80
|
-
const n = parseInt(value, 10);
|
|
81
|
-
return Number.isNaN(n) ? defaultValue : n;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
|
|
1
|
+
const { resolveApiKey } = require("../api");
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 统一成功输出
|
|
5
|
+
* @param {object} data - API 返回数据
|
|
6
|
+
* @param {"json"|"text"} format
|
|
7
|
+
* @param {function} [textFormatter] - text 模式的自定义格式化函数,接收 data 返回字符串
|
|
8
|
+
*/
|
|
9
|
+
function outputResult(data, format, textFormatter) {
|
|
10
|
+
if (format === "json") {
|
|
11
|
+
process.stdout.write(JSON.stringify({ success: true, data }, null, 2) + "\n");
|
|
12
|
+
} else {
|
|
13
|
+
const text = textFormatter ? textFormatter(data) : JSON.stringify(data, null, 2);
|
|
14
|
+
process.stdout.write(text + "\n");
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 统一错误输出
|
|
20
|
+
* @param {object} err - 错误对象(需有 message 属性)
|
|
21
|
+
* @param {"json"|"text"} format
|
|
22
|
+
*/
|
|
23
|
+
function outputError(err, format) {
|
|
24
|
+
const msg = err.message || String(err);
|
|
25
|
+
if (format === "json") {
|
|
26
|
+
process.stderr.write(JSON.stringify({ success: false, error: msg }, null, 2) + "\n");
|
|
27
|
+
} else {
|
|
28
|
+
process.stderr.write(`✗ 错误: ${msg}\n`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 解析 API Key,失败时输出错误并退出进程
|
|
34
|
+
* @param {string} [optionKey] - 命令行传入的 key
|
|
35
|
+
* @param {"json"|"text"} format
|
|
36
|
+
* @returns {string} 解析到的 API Key
|
|
37
|
+
*/
|
|
38
|
+
function resolveKeyOrExit(optionKey, format) {
|
|
39
|
+
const key = resolveApiKey(optionKey);
|
|
40
|
+
if (!key) {
|
|
41
|
+
outputError(
|
|
42
|
+
{ message: "缺少 API Key。请通过 -k 参数、AICONDUCTOR_API_KEY 环境变量或 .env 文件提供。" },
|
|
43
|
+
format
|
|
44
|
+
);
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
return key;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 统一 API 错误处理(不退出,由调用方决定退出码)
|
|
52
|
+
* @param {object} err - 错误对象
|
|
53
|
+
* @param {"json"|"text"} format
|
|
54
|
+
*/
|
|
55
|
+
function handleApiError(err, format) {
|
|
56
|
+
if (err.type === "http_error") {
|
|
57
|
+
outputError(
|
|
58
|
+
{ message: `API 返回错误 (HTTP ${err.statusCode}): ${err.body?.message || JSON.stringify(err.body)}` },
|
|
59
|
+
format
|
|
60
|
+
);
|
|
61
|
+
} else if (err.type === "network_error") {
|
|
62
|
+
outputError({ message: `网络错误: ${err.message}` }, format);
|
|
63
|
+
} else if (err.type === "validation_error") {
|
|
64
|
+
outputError({ message: `参数错误: ${err.message}` }, format);
|
|
65
|
+
} else if (err.type === "api_error") {
|
|
66
|
+
outputError({ message: err.message }, format);
|
|
67
|
+
} else {
|
|
68
|
+
outputError({ message: `未知错误: ${err.message || err}` }, format);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 安全解析整数,无效时返回 defaultValue
|
|
74
|
+
* @param {*} value
|
|
75
|
+
* @param {number} defaultValue
|
|
76
|
+
* @returns {number}
|
|
77
|
+
*/
|
|
78
|
+
function safeParseInt(value, defaultValue) {
|
|
79
|
+
if (value === undefined || value === null) return defaultValue;
|
|
80
|
+
const n = parseInt(value, 10);
|
|
81
|
+
return Number.isNaN(n) ? defaultValue : n;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 从文本(分享口令/带链接的句子)中抽出第一个 http(s) 链接。
|
|
86
|
+
*
|
|
87
|
+
* 小红书用户复制的内容形如「标题 https://xhslink.cn/o/xxx 复制内容,然后进入【小红书】查看相关笔记。」
|
|
88
|
+
* 平台侧的 share_text 是**原样透传上游**的,带中文口令必失败(2026-11-05 实测),所以链接必须本地抽。
|
|
89
|
+
* @param {string} text
|
|
90
|
+
* @returns {string|null}
|
|
91
|
+
*/
|
|
92
|
+
function extractUrlFromText(text) {
|
|
93
|
+
if (typeof text !== "string") return null;
|
|
94
|
+
const match = text.match(/https?:\/\/[^\s\u4e00-\u9fa5,。、;:!?()【】《》“”‘’]+/i);
|
|
95
|
+
if (!match) return null;
|
|
96
|
+
// 去掉 URL 尾部的英文标点(中文标点已在字符集里排除)
|
|
97
|
+
return match[0].replace(/[),.;:!?'"\]}>]+$/, "");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const XHS_ID_RE = /^[0-9a-f]{24}$/i;
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* 从用户输入里识别小红书笔记 ID(裸 ID / 长链 / 带链接的分享文案)。
|
|
104
|
+
* 短链(xhslink.cn)需先由 resolveXhsShortLink 换成长链。
|
|
105
|
+
* @param {string} text
|
|
106
|
+
* @returns {string|null}
|
|
107
|
+
*/
|
|
108
|
+
function parseXhsNoteId(text) {
|
|
109
|
+
if (typeof text !== "string") return null;
|
|
110
|
+
const raw = text.trim();
|
|
111
|
+
if (XHS_ID_RE.test(raw)) return raw;
|
|
112
|
+
const url = extractUrlFromText(raw);
|
|
113
|
+
if (!url) return null;
|
|
114
|
+
const match = url.match(/(?:explore|discovery\/item|note)\/([0-9a-f]{24})/i);
|
|
115
|
+
return match ? match[1] : null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* 从用户输入里识别小红书用户 ID(裸 ID / 主页链接)。
|
|
120
|
+
* @param {string} text
|
|
121
|
+
* @returns {string|null}
|
|
122
|
+
*/
|
|
123
|
+
function parseXhsUserId(text) {
|
|
124
|
+
if (typeof text !== "string") return null;
|
|
125
|
+
const raw = text.trim();
|
|
126
|
+
if (XHS_ID_RE.test(raw)) return raw;
|
|
127
|
+
const url = extractUrlFromText(raw);
|
|
128
|
+
if (!url) return null;
|
|
129
|
+
const match = url.match(/user\/profile\/([0-9a-f]{24})/i);
|
|
130
|
+
return match ? match[1] : null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* 小红书时间戳格式化:上游给的是**真 UTC epoch**(详情秒、笔记列表毫秒),展示需 +8 小时。
|
|
135
|
+
* 与抖音相反(抖音是「北京时间编码」,不要再加 8)。
|
|
136
|
+
* @param {number|string} value epoch 秒或毫秒
|
|
137
|
+
* @returns {string} `YYYY-MM-DD HH:mm:ss`,无效值返回空串
|
|
138
|
+
*/
|
|
139
|
+
function formatXhsTime(value) {
|
|
140
|
+
const n = Number(value);
|
|
141
|
+
if (!n || Number.isNaN(n)) return "";
|
|
142
|
+
const ms = n > 1e12 ? n : n * 1000;
|
|
143
|
+
return new Date(ms + 8 * 3600 * 1000).toISOString().replace("T", " ").slice(0, 19);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* 归一化为整数计数:上游同一平台里数字有 int、字符串、以及「1.2万」这类糊糊写法。
|
|
148
|
+
* @param {*} value
|
|
149
|
+
* @returns {number}
|
|
150
|
+
*/
|
|
151
|
+
function toCount(value) {
|
|
152
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : 0;
|
|
153
|
+
if (value === null || value === undefined) return 0;
|
|
154
|
+
const text = String(value).replace(/[,\s]/g, "");
|
|
155
|
+
const match = text.match(/^(\d+(?:\.\d+)?)(万|w)?\+?$/i);
|
|
156
|
+
if (!match) return 0;
|
|
157
|
+
const base = Number(match[1]);
|
|
158
|
+
return Math.round(match[2] ? base * 10000 : base);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
module.exports = {
|
|
162
|
+
outputResult,
|
|
163
|
+
outputError,
|
|
164
|
+
resolveKeyOrExit,
|
|
165
|
+
handleApiError,
|
|
166
|
+
safeParseInt,
|
|
167
|
+
extractUrlFromText,
|
|
168
|
+
parseXhsNoteId,
|
|
169
|
+
parseXhsUserId,
|
|
170
|
+
formatXhsTime,
|
|
171
|
+
toCount,
|
|
172
|
+
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 小红书达人打分(纯函数,不联网)。
|
|
3
|
+
*
|
|
4
|
+
* 公式与 `backend/api_docs/xhs_pgy_blogger.md` 的「注意事项 2」保持一致,
|
|
5
|
+
* HTTP 用户可依同一份公式自行计算。
|
|
6
|
+
*
|
|
7
|
+
* 三个维度满分 100:粉丝 40 / 互动率 30 / 百赞率 30。
|
|
8
|
+
* 某一段取数失败时,按「已拿到维度的满分之和」归一,并在结果里标 partial。
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const FAN_TIERS = [
|
|
12
|
+
[100000, 40],
|
|
13
|
+
[50000, 34],
|
|
14
|
+
[10000, 26],
|
|
15
|
+
[5000, 18],
|
|
16
|
+
[1000, 10],
|
|
17
|
+
];
|
|
18
|
+
const INTERACTION_TIERS = [
|
|
19
|
+
[20, 30],
|
|
20
|
+
[10, 24],
|
|
21
|
+
[5, 16],
|
|
22
|
+
[2, 8],
|
|
23
|
+
];
|
|
24
|
+
const HUNDRED_LIKE_TIERS = [
|
|
25
|
+
[50, 30],
|
|
26
|
+
[20, 24],
|
|
27
|
+
[10, 16],
|
|
28
|
+
[5, 8],
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 按阶梯取分:命中第一个 `value >= 门槛` 的档位,否则取兜底分。
|
|
33
|
+
* @param {number} value
|
|
34
|
+
* @param {Array<[number, number]>} tiers
|
|
35
|
+
* @param {number} fallback
|
|
36
|
+
* @returns {number}
|
|
37
|
+
*/
|
|
38
|
+
function pickTier(value, tiers, fallback) {
|
|
39
|
+
for (const [threshold, score] of tiers) {
|
|
40
|
+
if (value >= threshold) return score;
|
|
41
|
+
}
|
|
42
|
+
return fallback;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function toFinite(value) {
|
|
46
|
+
// null / undefined / 空串表示「没拿到」,不能当成 0 参与打分
|
|
47
|
+
if (value === null || value === undefined || value === "") return undefined;
|
|
48
|
+
const n = Number(value);
|
|
49
|
+
return Number.isFinite(n) ? n : undefined;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 计算达人评分。
|
|
54
|
+
* @param {{fansCount?: number, interactionRate?: number, hundredLikePercent?: number}} metrics
|
|
55
|
+
* @returns {{value: number, tier: string, basis: Array<{item: string, got: number, max: number}>, partial: boolean}}
|
|
56
|
+
*/
|
|
57
|
+
function scoreDaren(metrics = {}) {
|
|
58
|
+
const fansCount = toFinite(metrics.fansCount);
|
|
59
|
+
const interactionRate = toFinite(metrics.interactionRate);
|
|
60
|
+
const hundredLikePercent = toFinite(metrics.hundredLikePercent);
|
|
61
|
+
|
|
62
|
+
const dims = [];
|
|
63
|
+
if (fansCount !== undefined) {
|
|
64
|
+
dims.push({ item: "粉丝数", got: pickTier(fansCount, FAN_TIERS, 4), max: 40 });
|
|
65
|
+
}
|
|
66
|
+
if (interactionRate !== undefined) {
|
|
67
|
+
dims.push({ item: "互动率", got: pickTier(interactionRate, INTERACTION_TIERS, 0), max: 30 });
|
|
68
|
+
}
|
|
69
|
+
if (hundredLikePercent !== undefined) {
|
|
70
|
+
dims.push({ item: "百赞率", got: pickTier(hundredLikePercent, HUNDRED_LIKE_TIERS, 0), max: 30 });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!dims.length) {
|
|
74
|
+
return { value: 0, tier: "数据不足", basis: [], partial: true };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const got = dims.reduce((sum, d) => sum + d.got, 0);
|
|
78
|
+
const max = dims.reduce((sum, d) => sum + d.max, 0);
|
|
79
|
+
const value = Math.round((got / max) * 100);
|
|
80
|
+
const tier = value >= 80 ? "头部" : value >= 55 ? "腰部" : "尾部";
|
|
81
|
+
|
|
82
|
+
return { value, tier, basis: dims, partial: dims.length < 3 };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
module.exports = {
|
|
86
|
+
scoreDaren,
|
|
87
|
+
pickTier,
|
|
88
|
+
FAN_TIERS,
|
|
89
|
+
INTERACTION_TIERS,
|
|
90
|
+
HUNDRED_LIKE_TIERS,
|
|
91
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
const assert = require("node:assert/strict");
|
|
2
|
+
const test = require("node:test");
|
|
3
|
+
|
|
4
|
+
const { scoreDaren, pickTier } = require("./daren-score");
|
|
5
|
+
|
|
6
|
+
test("scoreDaren scores each tier boundary", () => {
|
|
7
|
+
// 头部:粉丝 10 万 + 互动率 20 + 百赞率 50 = 40+30+30
|
|
8
|
+
assert.equal(scoreDaren({ fansCount: 100000, interactionRate: 20, hundredLikePercent: 50 }).value, 100);
|
|
9
|
+
// 腰部:粉丝 1 万 + 互动率 10 + 百赞率 20 = 26+24+24 = 74
|
|
10
|
+
const waist = scoreDaren({ fansCount: 10000, interactionRate: 10, hundredLikePercent: 20 });
|
|
11
|
+
assert.equal(waist.value, 74);
|
|
12
|
+
assert.equal(waist.tier, "腰部");
|
|
13
|
+
// 尾部:粉丝 100 + 互动率 0 + 百赞率 0 = 4+0+0
|
|
14
|
+
const tail = scoreDaren({ fansCount: 100, interactionRate: 0, hundredLikePercent: 0 });
|
|
15
|
+
assert.equal(tail.value, 4);
|
|
16
|
+
assert.equal(tail.tier, "尾部");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("scoreDaren falls back to the lowest band just below a threshold", () => {
|
|
20
|
+
assert.equal(pickTier(99999, [[100000, 40], [50000, 34]], 4), 34);
|
|
21
|
+
assert.equal(pickTier(999, [[1000, 10]], 4), 4);
|
|
22
|
+
assert.equal(pickTier(1.9, [[2, 8]], 0), 0);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("scoreDaren normalises when a segment is missing", () => {
|
|
26
|
+
// 只有粉丝 + 互动率:40 + 30 = 70 / 70 → 100
|
|
27
|
+
const partial = scoreDaren({ fansCount: 200000, interactionRate: 25 });
|
|
28
|
+
assert.equal(partial.value, 100);
|
|
29
|
+
assert.equal(partial.partial, true);
|
|
30
|
+
// 只有粉丝 1 万:26/40 = 65 → 腰部
|
|
31
|
+
const onlyFans = scoreDaren({ fansCount: 10000 });
|
|
32
|
+
assert.equal(onlyFans.value, 65);
|
|
33
|
+
assert.equal(onlyFans.tier, "腰部");
|
|
34
|
+
assert.equal(onlyFans.basis.length, 1);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("scoreDaren reports 数据不足 when nothing usable is passed", () => {
|
|
38
|
+
const empty = scoreDaren({});
|
|
39
|
+
assert.equal(empty.value, 0);
|
|
40
|
+
assert.equal(empty.tier, "数据不足");
|
|
41
|
+
assert.equal(empty.partial, true);
|
|
42
|
+
assert.deepEqual(empty.basis, []);
|
|
43
|
+
|
|
44
|
+
// 非数字(如上游给 null/undefined/空串)同样视为缺失
|
|
45
|
+
assert.equal(scoreDaren({ fansCount: null, interactionRate: "abc" }).tier, "数据不足");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("scoreDaren basis keeps got/max per dimension for explainability", () => {
|
|
49
|
+
const result = scoreDaren({ fansCount: 5000, interactionRate: 5, hundredLikePercent: 10 });
|
|
50
|
+
assert.deepEqual(result.basis, [
|
|
51
|
+
{ item: "粉丝数", got: 18, max: 40 },
|
|
52
|
+
{ item: "互动率", got: 16, max: 30 },
|
|
53
|
+
{ item: "百赞率", got: 16, max: 30 },
|
|
54
|
+
]);
|
|
55
|
+
assert.equal(result.value, 50);
|
|
56
|
+
assert.equal(result.tier, "尾部");
|
|
57
|
+
});
|
package/AGENTS.md
DELETED
|
@@ -1,189 +0,0 @@
|
|
|
1
|
-
# 项目协作规范
|
|
2
|
-
|
|
3
|
-
## 项目概述
|
|
4
|
-
|
|
5
|
-
`aiconductor-cli` 是 AIConductor 平台 API 的命令行封装:Node.js >= 18、CommonJS、唯一运行时依赖 `commander`,发布到 npm 供 agent 与终端用户直接调用。七组命令覆盖网页发布、豆包搜索、热点简报、抖音数据、小红书数据、公众号数据;`.agents/skills/` 存放面向 agent 的工作流(pi 在项目受信任时会自动发现项目级 `.agents/skills/`),指导如何调用这些命令,其中对外发布的技能(`xhs-note-publish`、`douyin-script-extract`)还要满足 WorkBuddy 上架字段,见 `references/workbuddy技能规范.md`。`README.md` 是对外命令契约,本文档是开发与维护契约。
|
|
6
|
-
|
|
7
|
-
## 项目结构与模块组织
|
|
8
|
-
|
|
9
|
-
```text
|
|
10
|
-
bin/aiconductor-cli.js 唯一入口:注册全部命令、参数与选项(Commander.js)
|
|
11
|
-
src/api.js 平台 HTTP 客户端:端点常量、请求体映射、鉴权、文件上传
|
|
12
|
-
src/lib/cli-utils.js 输出与错误:outputResult / outputError / resolveKeyOrExit / handleApiError / safeParseInt
|
|
13
|
-
src/commands/
|
|
14
|
-
html_publish.js html publish
|
|
15
|
-
md_publish.js webpage publish | list | delete
|
|
16
|
-
web_search.js search web | image
|
|
17
|
-
content.js content briefing
|
|
18
|
-
douyin.js douyin 的 7 个子命令
|
|
19
|
-
xhs.js xhs hot-list | note-publish
|
|
20
|
-
wx.js wx article-data | history
|
|
21
|
-
src/api.test.js 全部自动化测试(api 映射 + 纯格式化/聚合函数)
|
|
22
|
-
api_docs/<endpoint>.md 每个平台接口的请求/响应/计费契约(`aiconductor-platform` 对应文件的整文件副本)
|
|
23
|
-
references/ 平台侧规范(workbuddy技能规范.md),不参与 CLI 运行;已 gitignore + npmignore,只在本机
|
|
24
|
-
.agents/skills/<name>/ SKILL.md(路由与执行)+ references/(按条件加载)+ 可选 scripts/;仓库里唯一入库的 .agents 子目录
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
推导规则:命令组名与文件名**不一一对应**(`webpage` → `md_publish.js`,`search` → `web_search.js`),找实现先看 `bin/aiconductor-cli.js` 的注册处。命令文件只做四件事:解析选项、调用 `src/api.js`、格式化输出、`process.exit`;共享逻辑进 `src/lib/cli-utils.js`,HTTP 与鉴权细节不允许出现在命令文件里。
|
|
28
|
-
|
|
29
|
-
## 配套项目:aiconductor-platform(接口契约的源头)
|
|
30
|
-
|
|
31
|
-
本 CLI 只是 `aiconductor-platform` 里 78 个插件端点的薄封装。接口行为、返回字段、计费口径、上游第三方服务的坑都在那边;CLI 侧遇到「字段对不上 / 端点是 404 / 参数被拒」,先回那边查,不要在本仓库猜。
|
|
32
|
-
|
|
33
|
-
- **路径**:`D:/Development/aiconductor-platform`(monorepo:`backend/` FastAPI 插件、`frontend/` Next.js 控制台,同机部署在平台那台 ECS 上)
|
|
34
|
-
- **路由推导**:`backend/plugins/<name>.py` → `/api/<name>`;`backend/plugins/<group>/<name>.py` → `/api/<group>/<name>`。删文件即下线路由,没有注册表
|
|
35
|
-
- **契约文档**:`backend/api_docs/<name>.md` 是唯一源头;`aiconductor-cli/api_docs/` 是它的**整文件副本**(例外:`china_stock_data.md` 只在 CLI 侧)。改契约先改 platform 再覆盖过来,不在 CLI 侧手改副本;已知 `xhs_note_publish.md` 两边已漂移,下次碰它时整文件对齐
|
|
36
|
-
- **平台侧的开发与运维规则**(新增插件、计费三件套、180s 超时预算、nginx 60s、发版自检清单、已验证的坑)全在 `aiconductor-platform/AGENTS.md`,动接口前读那里,本文档不复述
|
|
37
|
-
|
|
38
|
-
### 端点到命令的落点
|
|
39
|
-
|
|
40
|
-
| 要做的事 | 改哪里 |
|
|
41
|
-
|---|---|
|
|
42
|
-
| 确认真实行为、字段、上游依赖 | `aiconductor-platform/backend/plugins/<name>.py` + `api_docs/<name>.md` |
|
|
43
|
-
| 新增/修改接口契约 | platform 侧插件与 `api_docs`,再覆盖到 `aiconductor-cli/api_docs/` |
|
|
44
|
-
| CLI 接入 | `src/api.js`(端点常量 + `httpPost` + `stripBilling`)→ `src/commands/<组>.js` → `bin/aiconductor-cli.js` 注册 |
|
|
45
|
-
| 测试 | `src/api.test.js`,请求字段抄 `api_docs`,不抄实现 |
|
|
46
|
-
| 对外文档与技能 | `README.md`、`.agents/skills/*/SKILL.md` |
|
|
47
|
-
|
|
48
|
-
### 鉴权与计费的两侧分工
|
|
49
|
-
|
|
50
|
-
- **鉴权**:platform 框架会把 `Authorization: Bearer <key>` 与 body/query 里的 `api_key` 都塞进 `args.input.api_key`,所以插件两种都收;CLI 只照 `api_docs` 写的那一种发,不要"统一"。
|
|
51
|
-
- **计费**:单价、折扣、停用状态都在 platform 的 `api_services` 表(`service_code` 维度)。CLI 不写价格、不输出 `billing`;README 计费表里配置驱动的项写「以平台服务配置为准」,不写死数字。
|
|
52
|
-
- **失败面**:`wx_*` 系列把业务失败放在 HTTP 200 的响应体里(`success:false` + `message`),CLI 在 `api.js` 用 `throwOnBusinessFailure` 转成 `api_error`(退出码 1、错误进 stderr),不要改成"成功退出 + 空 data"。
|
|
53
|
-
|
|
54
|
-
### 联调限制(别假设能做)
|
|
55
|
-
|
|
56
|
-
- `API_HOST` 是 `src/api.js` 里的常量 `plugin.aiconductor.fun`,**没有环境变量覆盖**:CLI 现在无法指向本地后端(本地起服务是 `cd aiconductor-platform/backend && uvicorn main:app --reload --port 8000`)。要做本地联调得先给 CLI 加 host 覆盖,未做之前不要在文档里声称支持。
|
|
57
|
-
- 线上端点是否在线可直接查:`curl -s https://plugin.aiconductor.fun/api/plugins`(2026-11-04 实测 78 个)。判断"某端点不存在"以它为准,不以本文档的数字为准。
|
|
58
|
-
- 返回不对时先用 `-f json` 看原始响应:能对上就是 CLI 映射/格式化的问题,对不上或报上游错误就是 platform 侧契约或第三方服务变了。
|
|
59
|
-
|
|
60
|
-
### 公众号(wx_*)已接与未接
|
|
61
|
-
|
|
62
|
-
已接 2 个(`wx` 组):`wx_article_data` → `wx article-data`、`wx_history_articles` → `wx history`。
|
|
63
|
-
|
|
64
|
-
platform 侧还有、CLI 未接 10 个:`wx_rss_reader`、`flow_wx_history_articles`、`wx_access_token`、`wx_md_to_html`、`md_to_wxhtml`、`wx_image_upload`、`wx_draft_add`、`wx_draft_publish`、`users/wx_article_style_rewrite`、`wx_article_analysis_data_push`(最后一个只是扣费触发器,不返回数据)。`wx_rss_reader` 与 `flow_wx_history_articles` **用户明确不要**(2026-11-04),不要再当成待补项;接发布链路时注意 `wx_draft_add` 要先用 `wx_image_upload` 拿 `thumb_media_id`。
|
|
65
|
-
|
|
66
|
-
## 构建、测试与开发命令
|
|
67
|
-
|
|
68
|
-
| 命令 | 用途 |
|
|
69
|
-
|---|---|
|
|
70
|
-
| `npm install` | 安装唯一依赖 `commander`(有 lock) |
|
|
71
|
-
| `npm test` | `node --test`,27 个用例,离线打桩,不需要 API Key |
|
|
72
|
-
| `node bin/aiconductor-cli.js --help` | 不安装也能本地运行,核对命令面 |
|
|
73
|
-
| `node bin/aiconductor-cli.js <组> <子命令> --help` | 核对某个命令的参数与默认值;CLI 契约以它为准 |
|
|
74
|
-
| `npm pack --dry-run` | 确认发布内容:含 `src/**`(含测试)、`api_docs/`、`AGENTS.md`,不含 `.agents/` 与 `references/`(平台侧规范只在本机,已从 `.npmignore`/`.gitignore` 排掉) |
|
|
75
|
-
|
|
76
|
-
无构建步骤、无 lint/formatter、无 CI。
|
|
77
|
-
|
|
78
|
-
## 常见改动怎么做
|
|
79
|
-
|
|
80
|
-
### 新增或修改命令
|
|
81
|
-
|
|
82
|
-
1. 在对应 `src/commands/<组>.js` 加/改处理函数(薄封装,照现有文件风格)。
|
|
83
|
-
2. 需要新端点时先读 `api_docs/<endpoint>.md`,在 `src/api.js` 加端点常量与函数,复用 `httpPost` + `stripBilling`。
|
|
84
|
-
3. 在 `bin/aiconductor-cli.js` 注册子命令与选项——这是**唯一注册点**。`-k, --api-key`、`-f, --format` 每个命令各自声明,没有全局注入;可重复选项用 `collect`。
|
|
85
|
-
4. 跑 `node bin/aiconductor-cli.js <组> <子命令> --help` 核对命令面,跑 `npm test`。
|
|
86
|
-
5. 同步受影响产物:`README.md`(synopsis、选项表、示例)、对应的 `.agents/skills/*/SKILL.md`(命令、选项、`data.data.*` 路径)、`src/api.test.js`(改了请求映射或格式化时)。
|
|
87
|
-
|
|
88
|
-
### 新增平台接口
|
|
89
|
-
|
|
90
|
-
- 先到 platform 侧读插件与 `api_docs`(见「配套项目:aiconductor-platform」),搞清上游依赖与超时预算,再动 CLI。
|
|
91
|
-
- 鉴权有两套并存写法,照 `api_docs` 与相邻实现选,不要"统一":搜索、热榜、网页列表/删除、R2 上传、小红书发布、抖音视频搜索、`wx_*` 全系用 `Authorization: Bearer <key>`;发布类(`html_publish`、`md_to_html`)与多数抖音接口把 `api_key` 放进请求体。
|
|
92
|
-
- 业务失败可能出现在 HTTP 200 的响应体里:`api.js` 内抛 `api_error`(搜索、热榜,以及用 `throwOnBusinessFailure` 包一层的 `wx_*`),或命令层用 `throwOnApiFailure`(网页列表/删除)。新接口选一种并补测试。
|
|
93
|
-
- 响应先过 `stripBilling`,`billing` 不进任何输出。
|
|
94
|
-
|
|
95
|
-
### 维护 skill
|
|
96
|
-
|
|
97
|
-
- 技能放 `.agents/skills/<name>/`(2026-11-04 从 `.codex/skills/` 迁来,内容逐字未变,`git mv` 效果):pi 从 cwd 向上到仓库根发现项目级 `.agents/skills/`(`dist/core/package-manager.js` 的 `collectAncestorAgentsSkillDirs`),`.codex/skills/` 从来不是 pi 的发现路径。代价:项目里有 `.agents/skills/` 会触发**项目信任门禁**(`hasTrustRequiringProjectResources` 返回 true),未信任时这 11 个技能不加载。**这是故意的**(2026-11-04):本仓技能是**上架产物**(WorkBuddy / 技能仓库的发布件),不是本机开发时要用的上下文,所以 `~/.pi/agent/trust.json` 已给 `D:/Development/aiconductor-cli` 显式写 `false`——pi 不弹信任提示、技能不进上下文;需要某条技能内容时直接 `read .agents/skills/<name>/SKILL.md`,**不要为了加载它去信任项目**。
|
|
98
|
-
- `.gitignore` 用 `.agents/*` + `!.agents/skills/` 只放行技能,`.agents/ledger.md`、`.agents/plans/` 属本机脚手架不入库——**别把 `.agents/` 整目录 ignore**,那会让技能从版本控制里消失(`git status` 表现为 `.codex/` 全删、新目录全是 `??`)。
|
|
99
|
-
- 目录名、frontmatter `name`、实际调用的命令必须一致;`description` 写能力 + 用户原话触发 + 与相邻 skill 的边界(简报→content-briefing,单点搜索→doubao-search,网页→webpage-publish,抖音数据→douyin-toolbox,抖音文案→douyin-script-extract,抖音选题→douyin-topic-radar,抖音评论→douyin-comment-insight,抖音账号→douyin-account-analyze,小红书→xhs-note-publish,公众号发文列表→wx-account-articles,单篇公众号数据→wx-article-data)。
|
|
100
|
-
- `SKILL.md` 只放路由、决策、命令行、响应路径、安全规则;长选项表、安装与密钥恢复放 `references/`,并写明何时读。
|
|
101
|
-
- 默认 JSON 输出,文档写稳定的 `data.data.*` 路径(`outputResult` 会额外包一层 `{success, data}`)。
|
|
102
|
-
- 用户请求执行即等同授权:不要每次预检安装或密钥;安装说明只在需要时读一次,修复后重试原命令一次。
|
|
103
|
-
- 安装说明被复制成 11 份 `references/aiconductor-skills-setup.md`(技能是独立安装单元,无法跨目录引用);改它必须同步 11 份。改完跑 `md5sum .agents/skills/*/references/aiconductor-skills-setup.md | awk '{print $1}' | sort -u | wc -l`,结果必须是 **1**。
|
|
104
|
-
|
|
105
|
-
### 上架到 WorkBuddy
|
|
106
|
-
|
|
107
|
-
对外发布的技能比仓库内自用的多一层要求,规范全文在 `references/workbuddy技能规范.md`:
|
|
108
|
-
|
|
109
|
-
- **frontmatter 8 项必填**:`name`、`display_name`、`display_name_en`、`description`、`description_zh`、`description_en`、`version`、`author`;`author` 固定写 `"AI指挥家"`(带引号)。缺一项详情页就是残的。
|
|
110
|
-
- **`examples_zh` / `examples_en` 各 3 条**,展示在详情页「试试这样用」;用真能跑通的链接,别塞占位符。
|
|
111
|
-
- **正文顶部写 `## 概述`**,口吻是"默认已装好 CLI 与 API Key",只有出错时才引导去读 `references/aiconductor-skills-setup.md`。用户第一眼要看到能力,不是安装步骤。
|
|
112
|
-
- **别在正文里谈钱**:价格、单价、扣费规则、"免费"这类内容不进 `SKILL.md`——用户读技能时感觉自己在按次付费就会心疼,这是体验问题。该保留的行为约束改写成与钱无关的说法("重跑会再扣一次下载费" → "重跑等于从下载重来一遍");但故障解释里该说还得说,"余额不足"能帮用户定位问题。
|
|
113
|
-
- **子资源用 Markdown 链接** `[references/x.md](references/x.md)`,不用规范文档里的 `@references/x.md`:全库实测 0 处 `@`、549 处链接,已上架的 `xhs-note-publish` 也是链接。
|
|
114
|
-
- **交付物就是可上传的目录本身**:`SKILL.md` + `references/`(必要时 `scripts/`)随目录走,不依赖仓库内其他路径——技能是独立安装单元。交付时另附一份可直接复制进后台的清单:中英名、中英简介、3 条案例、作者、版本。
|
|
115
|
-
|
|
116
|
-
## 关键机制(改代码前必读)
|
|
117
|
-
|
|
118
|
-
- **输出分层**:`outputResult` 在 JSON 模式打印 `{success:true, data:<API 原始响应>}`,服务端字段落在 `data.data.*`(如 `content briefing` 的 `data.data.sources`);text 模式只打印自定义 formatter 的文本。错误一律走 stderr:JSON 为 `{success:false, error}`,text 为 `✗ 错误: ...`。
|
|
119
|
-
- **退出码**:成功 0、失败 1,每个处理函数结尾显式 `process.exit()`,因此 `await` 之后的收尾逻辑不会执行。不带参数调用时 Commander 直接向 stderr 打帮助并退出 1(bin 末尾的 `outputHelp()` 在该路径不可达)。
|
|
120
|
-
- **错误分类**:`http_error`(状态码 >= 400)、`network_error`、`validation_error`(本地参数校验)、`api_error`(HTTP 200 但业务失败);`handleApiError` 按 `.type` 生成中文提示。新抛错带 `.type`。
|
|
121
|
-
- **传输层**:请求打到 `plugin.aiconductor.fun/api/<endpoint>`。多数走 HTTPS;`hot_list_mixed` 与 `xhs_hot_list` 走明文 HTTP(`transport = http`,`api_docs` 也写作 `http://`,测试锁定 `port 80`)。线上 HTTPS 同样可达,属历史选择,别当成必须。
|
|
122
|
-
- **本地先校验**:`validatePositiveInt` / `validateInt` 与各命令的显式检查负责区间(`limit` 1–30、搜索 `count` 网页 ≤ 50 / 图片 ≤ 5、图文 ≤ 18 张等),失败不请求、不计费。
|
|
123
|
-
- **`webpage publish` 分流**:按扩展名或内容特征判定 `md`/`html`(`--type` 可强制)。html 分支直接打 `html_publish`,md 分支打 `md_to_html` 并支持 `--copy-button`;两者是不同端点。
|
|
124
|
-
- **小红书发布是两段式**:本地文件或 Base64 先经 `r2Upload` 换成临时公开 URL(`valid_days` 仅 1 或 7,默认 7;图片单张 ≤ 32MB,本地视频 ≤ 100MB),再调用计费接口生成 H5 页与二维码。Base64 参数支持 `@文件路径`。
|
|
125
|
-
- **热点简报是部分成功语义**:热榜与小红书热榜并行请求,任一成功即返回 `success:true`,失败写入 `data.failed_sources` / `data.failed_services`;仅两个服务都失败才退出 1。不要简化成"全有或全无"。
|
|
126
|
-
- **`wx` 组线上真实字段**(2026-11-04 实测调用):`wx_article_data` 返回 `read` / `zan` / `looking`(在看)/ `share_num` / `collect_num` / `comment_count`;`wx_history_articles` 用 `--name` 定位时业务层只有 `mp_nickname` 与 `mp_ghid`——**`api_docs` 写的 `mp_wxid` / `head_img` 实际不返回**;文章项字段与文档一致(`post_time_str` / `read` / `zan` / `original` / `url` / `digest`)。改 formatter 按实测字段,不要按文档新增字段。
|
|
127
|
-
- **`wx_history_articles` 的上游失败藏在 `data.code`**:平台侧仍回 `success:true`,只有 `data.code` 非 0 才是失败(实测 `107` = 名称/链接对不上,`20001` = 平台侧上游故障),`src/api.js` 的 `throwOnWxHistoryFailure` 把它转成 `api_error`(退出码 1)。别用「数组为空」推断「这个号没发文」。
|
|
128
|
-
- **抖音字段单位不统一**:`video-search` 的 `video.duration` 是**毫秒**,`user-videos` 的 `duration` 是 `"MM:SS"` 字符串——同名不同单位,跨端点复用代码必翻车。
|
|
129
|
-
- **抖音时间戳是北京时间编码**:`create_time` 按 UTC 渲染出来就是北京时间,**不要再 +8 小时**(2026-10-28 实测 `video-search`/`video-comments`/`user-videos` 三处:+8 会让半小时前发的视频落到 7 小时后)。`user-videos` 另带 `create_time_formatted`,同口径。
|
|
130
|
-
- **密钥解析**:`-k` > `AICONDUCTOR_API_KEY` > **当前工作目录**的 `.env`(不向上查找);缺失时 `resolveKeyOrExit` 打印错误并退出 1。
|
|
131
|
-
|
|
132
|
-
## 禁区
|
|
133
|
-
|
|
134
|
-
- 不输出、不提交 `billing`、API Key、`.env` 内容、Base64 素材;密钥只走环境变量或命令行参数。
|
|
135
|
-
- 不手改 `api_docs/` 去迁就实现——它是接口契约(源头在 platform),契约变了才更新,并同时改测试。
|
|
136
|
-
- 不把 platform 侧计费单价写进 CLI 代码、README 或测试断言(价格在 `api_services` 表)。
|
|
137
|
-
- 不把 `hot_list_mixed` / `xhs_hot_list` 的明文 HTTP 顺手改成 HTTPS(测试锁定 `port 80`;确需改则测试、文档一起改并验证线上)。
|
|
138
|
-
- 不在命令文件里重写 HTTP、鉴权或格式化,不在 `src/lib` 之外复制共享工具。
|
|
139
|
-
- 不新增运行时依赖、不引入 ESM(当前只有 `commander`,用原生 `http/https`)。
|
|
140
|
-
- 不在 `src/` 内注册命令;命令注册只在 `bin/aiconductor-cli.js`。
|
|
141
|
-
|
|
142
|
-
## Agent 自主决策与复用约定
|
|
143
|
-
|
|
144
|
-
- 默认复用:`httpPost` / `stripBilling` / `validatePositiveInt` / `outputResult` / `handleApiError` / `resolveKeyOrExit`;新增代码照同目录文件写,不另起一套。
|
|
145
|
-
- 默认选型:CommonJS + `commander` + 原生 `http/https` + `node:test`;不引入 axios、node-fetch 或测试框架。
|
|
146
|
-
- 目录落点:一组命令一个 `src/commands/<组>.js`;HTTP 接口进 `src/api.js`;纯函数(格式化、聚合)留在命令文件内并导出以便测试。
|
|
147
|
-
- 直接做:读改代码、跑 `npm test`、跑 `--help`、`npm pack --dry-run`。
|
|
148
|
-
- 先确认:`npm publish`/发版、`git push`、技能上架到 WorkBuddy、任何带 API Key 的真实计费调用(`search`、`douyin`、`xhs`、`content`、`wx`)、`webpage delete`(不可逆,会失效公开链接)、批量改写 skills。
|
|
149
|
-
- 缺 API Key 时提示配置方式,不伪造、不复用示例输出。接任务先补齐背景、交付物、不做边界、约束、验收;"优化一下/完善一下"先翻译成可测量标准(哪条命令、哪个字段、什么退出码)。
|
|
150
|
-
|
|
151
|
-
## 最短验证闭环
|
|
152
|
-
|
|
153
|
-
`npm test`(27 个用例,离线打桩,不需要 API Key)。改动涉及 `bin/`、`README.md`、skill 文字或新增命令时,再补一条人工核对:`node bin/aiconductor-cli.js <组> <子命令> --help`——`bin/` 的接线没有自动化测试覆盖。
|
|
154
|
-
|
|
155
|
-
## 任务执行与报告约定
|
|
156
|
-
|
|
157
|
-
复杂任务先定位根因或明确方案,再拆分实施与验证;≤3 文件的局部改动直接改并验证。步骤 ≥3 用 todo 跟踪,一次一个 in_progress,做完立即标 completed。交付用通俗语言报告改了什么、验证结果(命令 + 实际输出)、对外影响(命令面、计费、技能)。
|
|
158
|
-
|
|
159
|
-
## 编码风格与命名约定
|
|
160
|
-
|
|
161
|
-
- 2 空格缩进,无 tab;CommonJS(`require` / `module.exports`),不写 ESM。
|
|
162
|
-
- JS 标识符 camelCase;请求字段用 API 的 snake_case(`need_content`、`max_pages`),映射在 `api.js` 完成,命令层保留 camelCase 选项名。
|
|
163
|
-
- 导出函数写 JSDoc(参数 + 返回类型);`const` 优先,必要时 `let`。
|
|
164
|
-
- 用户可见文本用中文;默认 JSON 输出,text 输出用 `✓` / `✗` 前缀与对齐字段名。
|
|
165
|
-
- 无 formatter/linter,跟随相邻文件风格。
|
|
166
|
-
|
|
167
|
-
## 测试指南
|
|
168
|
-
|
|
169
|
-
- 框架:Node 内置 `node:test` + `node:assert/strict`,`npm test` 一次跑完;测试与模块同目录(目前集中在 `src/api.test.js`)。
|
|
170
|
-
- 网络必须打桩:`captureRequest` 替换 `https.request` / `http.request`,断言 `options.path`、`options.port`、`Authorization` 与请求体,同时校验返回值。测试不依赖 API Key、不发真实请求。
|
|
171
|
-
- 期望值独立于实现:请求字段以 `api_docs/<endpoint>.md` 为准;text 输出断言关键行(如 `结果数`、`URL`、`H5 地址`),不用整段快照。
|
|
172
|
-
- CLI 契约改动覆盖三类:`--help` 暴露的命令面(人工核对)、成功响应的解析与格式化(单测)、校验/鉴权/上游失败的非零退出(单测 + 一次手工失败调用)。
|
|
173
|
-
|
|
174
|
-
## 提交与 PR 规范
|
|
175
|
-
|
|
176
|
-
- 格式 `<tag>: <描述>`,tag 取自既有历史:`feat:`、`fix:`、`refactor:`、`docs:`、`skills:`、`search:`、`content:`、`release:`;agent 提交在前面加 `[Agent]`(如 `[Agent] release(cli): 1.4.0 —— ...`),描述可用中文。
|
|
177
|
-
- 发版提交只改 `package.json` + `package-lock.json` 的版本号;发版前先跑 `npm test`。
|
|
178
|
-
- 新增或修改 skill 必须随 `.agents/skills/**` 一起进 Git(`.npmignore` 已排除 `.agents/`,所以技能在仓库里版本化、不进 npm 包)。
|
|
179
|
-
- 历史只有单作者、无 PR 记录,可直接在 `master` 提交;若走 PR,描述写清命令面/API 影响与验证命令,保持 diff 聚焦。
|
|
180
|
-
|
|
181
|
-
## 安全与配置
|
|
182
|
-
|
|
183
|
-
- 只需一个环境变量 `AICONDUCTOR_API_KEY`(只写变量名,值不入库);解析顺序 `-k` > 环境变量 > 当前目录 `.env`。
|
|
184
|
-
- `.env`、`*.log`、`node_modules/` 已在 `.gitignore`;Base64 素材文件同样不得入库。
|
|
185
|
-
- 平台接口域名与各端点契约见 `api_docs/`,不要在代码外另维护第二份接口清单。
|
|
186
|
-
|
|
187
|
-
## 文档维护约定(常驻,不删除)
|
|
188
|
-
|
|
189
|
-
当本文档缺失/过时导致你走弯路时,在任务收尾顺手更新对应章节,只改受影响的最小范围。公共接口、命令、目录约定变化必须同步更新;一次性的会话经验不写入,只收下次还会踩的坑。
|