@9000ai/cli 0.5.3 → 0.5.5
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/CHANGELOG.md +7 -0
- package/dist/client.d.ts +1 -0
- package/dist/client.js +23 -0
- package/dist/commands/config.js +8 -4
- package/dist/commands/init.js +5 -5
- package/dist/commands/monitor.js +2 -1
- package/dist/commands/search.js +82 -18
- package/dist/commands/skill.js +17 -15
- package/dist/commands/update.js +4 -3
- package/dist/config.d.ts +2 -0
- package/dist/config.js +12 -1
- package/dist/index.js +1 -1
- package/dist/output.d.ts +1 -0
- package/dist/output.js +10 -0
- package/dist/postinstall.js +2 -2
- package/package.json +1 -1
- package/skills/9000AI-hub/SKILL.md +2 -1
- package/skills/douyin-monitor/SKILL.md +6 -3
- package/skills/douyin-topic-discovery/SKILL.md +5 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
## What's new in v0.5.5
|
|
2
|
+
|
|
3
|
+
- `--format csv` — search keyword, batch-result, monitor fetch 均支持 CSV 输出,省 token
|
|
4
|
+
- `--skills-path` — `config set --skills-path <dir>` 自定义 skills 安装路径
|
|
5
|
+
- `9000ai update --path <dir>` — 指定 skills 同步目标路径
|
|
6
|
+
- 统一字段名:monitor fetch 返回 `desc`/`create_time`(与 search 一致)
|
|
7
|
+
|
|
1
8
|
## What's new in v0.5.3
|
|
2
9
|
|
|
3
10
|
- `9000ai update` — sync skills after npm update, shows what changed
|
package/dist/client.d.ts
CHANGED
package/dist/client.js
CHANGED
|
@@ -64,6 +64,25 @@ export async function pollUntilDone(path, opts) {
|
|
|
64
64
|
}
|
|
65
65
|
export function printJson(data, opts) {
|
|
66
66
|
let output = data;
|
|
67
|
+
// --format csv: extract items array, pick fields, output as CSV
|
|
68
|
+
if (opts?.format === "csv") {
|
|
69
|
+
const arr = extractArray(output);
|
|
70
|
+
if (arr && arr.length > 0) {
|
|
71
|
+
const fieldList = opts.fields
|
|
72
|
+
? opts.fields.split(",").map((f) => f.trim())
|
|
73
|
+
: Object.keys(arr[0]);
|
|
74
|
+
const picked = arr.map((item) => pick(item, fieldList));
|
|
75
|
+
const header = fieldList.join(",");
|
|
76
|
+
const lines = picked.map((row) => fieldList.map((k) => {
|
|
77
|
+
const v = String(row[k] ?? "");
|
|
78
|
+
return v.includes(",") || v.includes('"') || v.includes("\n")
|
|
79
|
+
? `"${v.replace(/"/g, '""')}"`
|
|
80
|
+
: v;
|
|
81
|
+
}).join(","));
|
|
82
|
+
console.log([header, ...lines].join("\n"));
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
67
86
|
// --fields: extract specified fields from items/data
|
|
68
87
|
if (opts?.fields) {
|
|
69
88
|
const fieldList = opts.fields.split(",").map((f) => f.trim());
|
|
@@ -83,6 +102,10 @@ export function printJson(data, opts) {
|
|
|
83
102
|
function pickFields(data, fields) {
|
|
84
103
|
if (!data || typeof data !== "object")
|
|
85
104
|
return data;
|
|
105
|
+
// Bare array — pick from each element
|
|
106
|
+
if (Array.isArray(data)) {
|
|
107
|
+
return data.map((item) => pick(item, fields));
|
|
108
|
+
}
|
|
86
109
|
const obj = data;
|
|
87
110
|
// If response has code/message/data wrapper, dig into data
|
|
88
111
|
if ("code" in obj && "data" in obj) {
|
package/dist/commands/config.js
CHANGED
|
@@ -1,19 +1,22 @@
|
|
|
1
|
-
import { loadConfig, saveConfig, resolveBaseUrl } from "../config.js";
|
|
1
|
+
import { loadConfig, saveConfig, resolveBaseUrl, resolveSkillsPath } from "../config.js";
|
|
2
2
|
export function registerConfigCommands(parent) {
|
|
3
3
|
const cmd = parent.command("config").description("Manage CLI configuration");
|
|
4
4
|
cmd
|
|
5
5
|
.command("set")
|
|
6
|
-
.description("Set base URL and/or
|
|
6
|
+
.description("Set base URL, API key, and/or skills path")
|
|
7
7
|
.option("--base-url <url>", "Hub service address")
|
|
8
8
|
.option("--api-key <key>", "API key for authentication")
|
|
9
|
+
.option("--skills-path <dir>", "Directory where skills are installed")
|
|
9
10
|
.action((opts) => {
|
|
10
11
|
const patch = {};
|
|
11
12
|
if (opts.baseUrl)
|
|
12
13
|
patch.base_url = opts.baseUrl;
|
|
13
14
|
if (opts.apiKey)
|
|
14
15
|
patch.api_key = opts.apiKey;
|
|
16
|
+
if (opts.skillsPath)
|
|
17
|
+
patch.skills_path = opts.skillsPath;
|
|
15
18
|
if (Object.keys(patch).length === 0) {
|
|
16
|
-
console.error("Provide at least --base-url or --
|
|
19
|
+
console.error("Provide at least --base-url, --api-key, or --skills-path");
|
|
17
20
|
process.exit(1);
|
|
18
21
|
}
|
|
19
22
|
saveConfig(patch);
|
|
@@ -25,6 +28,7 @@ export function registerConfigCommands(parent) {
|
|
|
25
28
|
.action(() => {
|
|
26
29
|
const cfg = loadConfig();
|
|
27
30
|
const baseUrl = resolveBaseUrl();
|
|
28
|
-
|
|
31
|
+
const skillsPath = resolveSkillsPath();
|
|
32
|
+
console.log(JSON.stringify({ ...cfg, resolved_base_url: baseUrl, resolved_skills_path: skillsPath }, null, 2));
|
|
29
33
|
});
|
|
30
34
|
}
|
package/dist/commands/init.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, copyFileSync, readFileSync, readdirSync, statSync } from "fs";
|
|
2
|
-
import { homedir } from "os";
|
|
3
2
|
import { join, relative } from "path";
|
|
4
|
-
|
|
3
|
+
import { resolveSkillsPath } from "../config.js";
|
|
5
4
|
/** User data directories — never overwrite even with --force */
|
|
6
5
|
const USER_DATA_DIRS = ["profile", "claims", "inbox", "projects"];
|
|
7
6
|
function filesEqual(a, b) {
|
|
@@ -72,9 +71,10 @@ export function registerInitCommand(program) {
|
|
|
72
71
|
.option("--force", "Update existing files (except profile/) to latest version")
|
|
73
72
|
.action((opts) => {
|
|
74
73
|
const dest = opts.path;
|
|
74
|
+
const templatesDir = join(resolveSkillsPath(), "9000AI-hub", "init", "templates");
|
|
75
75
|
// Check templates exist
|
|
76
|
-
if (!existsSync(
|
|
77
|
-
console.error(`Error: Templates not found at ${
|
|
76
|
+
if (!existsSync(templatesDir)) {
|
|
77
|
+
console.error(`Error: Templates not found at ${templatesDir}\n` +
|
|
78
78
|
`Run: npm install -g @9000ai/cli`);
|
|
79
79
|
process.exit(1);
|
|
80
80
|
}
|
|
@@ -82,7 +82,7 @@ export function registerInitCommand(program) {
|
|
|
82
82
|
if (!existsSync(dest)) {
|
|
83
83
|
mkdirSync(dest, { recursive: true });
|
|
84
84
|
}
|
|
85
|
-
const { created, updated, skipped, outdated } = copyTemplates(
|
|
85
|
+
const { created, updated, skipped, outdated } = copyTemplates(templatesDir, dest, dest, !!opts.force);
|
|
86
86
|
// Output summary
|
|
87
87
|
if (created.length > 0) {
|
|
88
88
|
console.log(`Created ${created.length} files:`);
|
package/dist/commands/monitor.js
CHANGED
|
@@ -110,6 +110,7 @@ export function registerMonitorCommands(parent) {
|
|
|
110
110
|
.requiredOption("--sec-user <id>", "User sec_user_id or homepage URL")
|
|
111
111
|
.option("--count <n>", "Number of videos to fetch", "20")
|
|
112
112
|
.option("--sort <n>", "Sort: 0=newest 1=hottest", "0")
|
|
113
|
+
.option("--format <fmt>", "Output format: csv|json", "json")
|
|
113
114
|
.option("--fields <list>", "Comma-separated fields to extract")
|
|
114
115
|
.option("--compact", "One JSON object per line")
|
|
115
116
|
.action(async (opts) => {
|
|
@@ -119,6 +120,6 @@ export function registerMonitorCommands(parent) {
|
|
|
119
120
|
sort_type: opts.sort,
|
|
120
121
|
});
|
|
121
122
|
const data = await request({ method: "GET", path: `/api/v1/douyin/monitor/user-posts?${params}` });
|
|
122
|
-
printJson(data, { fields: opts.fields, compact: opts.compact });
|
|
123
|
+
printJson(data, { fields: opts.fields, compact: opts.compact, format: opts.format });
|
|
123
124
|
});
|
|
124
125
|
}
|
package/dist/commands/search.js
CHANGED
|
@@ -1,30 +1,61 @@
|
|
|
1
1
|
import { request, printJson, pollUntilDone } from "../client.js";
|
|
2
|
-
import { writeJson, writeTsv, listOutputFiles, readOutputJson, timestampSlug } from "../output.js";
|
|
2
|
+
import { writeJson, writeTsv, listOutputFiles, readOutputJson, timestampSlug, formatCsv } from "../output.js";
|
|
3
|
+
const ALL_BOARD_TYPES = ["hot", "entertainment", "society", "seeding", "city", "challenge"];
|
|
3
4
|
export function registerSearchCommands(parent) {
|
|
4
5
|
const cmd = parent.command("search").description("Douyin topic discovery — trending boards and keyword search");
|
|
5
6
|
cmd
|
|
6
7
|
.command("hot")
|
|
7
8
|
.description("Fetch Douyin trending board")
|
|
8
|
-
.option("--type <type>", "Board type: hot|city|seeding|entertainment|society|challenge", "hot")
|
|
9
|
+
.option("--type <type>", "Board type: hot|city|seeding|entertainment|society|challenge or comma-separated or 'all'", "hot")
|
|
9
10
|
.option("--count <n>", "Number of items", "20")
|
|
10
11
|
.option("--city <city>", "City filter (only for type=city)")
|
|
12
|
+
.option("--format <fmt>", "Output format: csv|json", "csv")
|
|
13
|
+
.option("--fields <list>", "Comma-separated fields to extract")
|
|
14
|
+
.option("--compact", "One JSON object per line (only for --format json)")
|
|
11
15
|
.action(async (opts) => {
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
const types = opts.type === "all" ? ALL_BOARD_TYPES : opts.type.split(",").map((t) => t.trim());
|
|
17
|
+
const allItems = [];
|
|
18
|
+
const results = [];
|
|
19
|
+
for (const boardType of types) {
|
|
20
|
+
const params = new URLSearchParams({ type: boardType, count: opts.count });
|
|
21
|
+
if (opts.city)
|
|
22
|
+
params.set("city", opts.city);
|
|
23
|
+
const data = await request({ method: "GET", path: `/api/v1/douyin/discovery/hot-board?${params}` });
|
|
24
|
+
const resp = data;
|
|
25
|
+
const inner = resp.data;
|
|
26
|
+
const items = (inner?.items ?? []);
|
|
27
|
+
// tag each item with board_type when fetching multiple boards
|
|
28
|
+
if (types.length > 1) {
|
|
29
|
+
items.forEach((item) => { item.board_type = boardType; });
|
|
30
|
+
}
|
|
31
|
+
results.push({ type: boardType, items });
|
|
32
|
+
allItems.push(...items);
|
|
33
|
+
}
|
|
34
|
+
// write files
|
|
19
35
|
const slug = timestampSlug();
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
36
|
+
const mergedData = types.length === 1
|
|
37
|
+
? results[0].items
|
|
38
|
+
: { boards: results.map((r) => ({ type: r.type, count: r.items.length })), items: allItems };
|
|
39
|
+
writeJson("latest_hot.json", mergedData);
|
|
40
|
+
writeJson(`latest_hot_${slug}.json`, mergedData);
|
|
41
|
+
if (allItems.length > 0) {
|
|
42
|
+
writeTsv("latest_hot.tsv", allItems);
|
|
43
|
+
writeTsv(`latest_hot_${slug}.tsv`, allItems);
|
|
44
|
+
}
|
|
45
|
+
// output
|
|
46
|
+
const boardLabel = types.length === 1 ? types[0] : types.join(",");
|
|
47
|
+
console.error(`Fetched ${allItems.length} items from [${boardLabel}] → output/latest_hot.json`);
|
|
48
|
+
if (opts.format === "csv") {
|
|
49
|
+
const csvFields = opts.fields
|
|
50
|
+
? opts.fields.split(",").map((f) => f.trim())
|
|
51
|
+
: types.length > 1
|
|
52
|
+
? ["board_type", "rank", "word", "hot_value"]
|
|
53
|
+
: ["rank", "word", "hot_value"];
|
|
54
|
+
console.log(formatCsv(allItems, csvFields));
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
printJson(allItems, { fields: opts.fields, compact: opts.compact });
|
|
25
58
|
}
|
|
26
|
-
console.log(`Fetched ${items.length} items → output/latest_hot.json`);
|
|
27
|
-
printJson(inner);
|
|
28
59
|
});
|
|
29
60
|
cmd
|
|
30
61
|
.command("keyword")
|
|
@@ -37,6 +68,7 @@ export function registerSearchCommands(parent) {
|
|
|
37
68
|
.option("--min-likes <n>", "Minimum likes", "0")
|
|
38
69
|
.option("--min-comments <n>", "Minimum comments", "0")
|
|
39
70
|
.option("--wait", "Poll until batch completes then print results")
|
|
71
|
+
.option("--format <fmt>", "Output format: csv|json", "json")
|
|
40
72
|
.option("--fields <list>", "Comma-separated fields to extract (used with --wait)")
|
|
41
73
|
.option("--compact", "One JSON object per line (used with --wait)")
|
|
42
74
|
.action(async (keywords, opts) => {
|
|
@@ -83,12 +115,13 @@ export function registerSearchCommands(parent) {
|
|
|
83
115
|
writeTsv(`latest_search_${slug}.tsv`, items);
|
|
84
116
|
console.error(`${items.length} items → output/latest_search.json`);
|
|
85
117
|
}
|
|
86
|
-
printJson(resultInner, { fields: opts.fields, compact: opts.compact });
|
|
118
|
+
printJson(resultInner, { fields: opts.fields, compact: opts.compact, format: opts.format });
|
|
87
119
|
});
|
|
88
120
|
cmd
|
|
89
121
|
.command("batch-result")
|
|
90
122
|
.description("Query async search batch results")
|
|
91
123
|
.requiredOption("--batch-id <id>", "Batch ID")
|
|
124
|
+
.option("--format <fmt>", "Output format: csv|json", "json")
|
|
92
125
|
.option("--fields <list>", "Comma-separated fields to extract (e.g. desc,author_name,likes,video_url)")
|
|
93
126
|
.option("--compact", "One JSON object per line, no indentation")
|
|
94
127
|
.action(async (opts) => {
|
|
@@ -107,7 +140,38 @@ export function registerSearchCommands(parent) {
|
|
|
107
140
|
writeTsv(`latest_search_${slug}.tsv`, items);
|
|
108
141
|
console.log(`${items.length} items → output/latest_search.json`);
|
|
109
142
|
}
|
|
110
|
-
printJson(inner, { fields: opts.fields, compact: opts.compact });
|
|
143
|
+
printJson(inner, { fields: opts.fields, compact: opts.compact, format: opts.format });
|
|
144
|
+
});
|
|
145
|
+
cmd
|
|
146
|
+
.command("zhihu-hot")
|
|
147
|
+
.description("Fetch Zhihu (知乎) trending hot list")
|
|
148
|
+
.option("--count <n>", "Number of items", "50")
|
|
149
|
+
.option("--format <fmt>", "Output format: csv|json", "csv")
|
|
150
|
+
.option("--fields <list>", "Comma-separated fields to extract")
|
|
151
|
+
.option("--compact", "One JSON object per line (only for --format json)")
|
|
152
|
+
.action(async (opts) => {
|
|
153
|
+
const params = new URLSearchParams({ count: opts.count });
|
|
154
|
+
const data = await request({ method: "GET", path: `/api/v1/zhihu/discovery/hot-list?${params}` });
|
|
155
|
+
const resp = data;
|
|
156
|
+
const inner = resp.data;
|
|
157
|
+
const items = (inner?.items ?? []);
|
|
158
|
+
const slug = timestampSlug();
|
|
159
|
+
writeJson("latest_zhihu_hot.json", inner);
|
|
160
|
+
writeJson(`latest_zhihu_hot_${slug}.json`, inner);
|
|
161
|
+
if (items.length > 0) {
|
|
162
|
+
writeTsv("latest_zhihu_hot.tsv", items);
|
|
163
|
+
writeTsv(`latest_zhihu_hot_${slug}.tsv`, items);
|
|
164
|
+
}
|
|
165
|
+
console.error(`Fetched ${items.length} items from [知乎热榜] → output/latest_zhihu_hot.json`);
|
|
166
|
+
if (opts.format === "csv") {
|
|
167
|
+
const csvFields = opts.fields
|
|
168
|
+
? opts.fields.split(",").map((f) => f.trim())
|
|
169
|
+
: ["rank", "title", "detail_text", "answer_count"];
|
|
170
|
+
console.log(formatCsv(items, csvFields));
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
printJson(items, { fields: opts.fields, compact: opts.compact });
|
|
174
|
+
}
|
|
111
175
|
});
|
|
112
176
|
cmd
|
|
113
177
|
.command("list-output")
|
package/dist/commands/skill.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { existsSync, readFileSync, readdirSync } from "fs";
|
|
2
|
-
import { homedir } from "os";
|
|
3
2
|
import { join, resolve } from "path";
|
|
4
|
-
|
|
5
|
-
function getSkillPath(name) {
|
|
6
|
-
return resolve(
|
|
3
|
+
import { resolveSkillsPath } from "../config.js";
|
|
4
|
+
function getSkillPath(skillsDir, name) {
|
|
5
|
+
return resolve(skillsDir, name, "SKILL.md");
|
|
7
6
|
}
|
|
8
7
|
export function registerSkillCommands(program) {
|
|
9
8
|
const skill = program.command("skill").description("Skill management");
|
|
@@ -12,19 +11,20 @@ export function registerSkillCommands(program) {
|
|
|
12
11
|
.command("list")
|
|
13
12
|
.description("List all installed skills")
|
|
14
13
|
.action(() => {
|
|
15
|
-
|
|
14
|
+
const skillsDir = resolveSkillsPath();
|
|
15
|
+
if (!existsSync(skillsDir)) {
|
|
16
16
|
console.log("No skills installed. Run: npm install -g @9000ai/cli");
|
|
17
17
|
return;
|
|
18
18
|
}
|
|
19
19
|
try {
|
|
20
|
-
const dirs = readdirSync(
|
|
20
|
+
const dirs = readdirSync(skillsDir).filter((d) => existsSync(join(skillsDir, d, "SKILL.md")));
|
|
21
21
|
if (dirs.length === 0) {
|
|
22
22
|
console.log("No skills found.");
|
|
23
23
|
return;
|
|
24
24
|
}
|
|
25
|
-
console.log(`Installed skills (${
|
|
25
|
+
console.log(`Installed skills (${skillsDir}):\n`);
|
|
26
26
|
dirs.forEach((d) => {
|
|
27
|
-
const path = join(
|
|
27
|
+
const path = join(skillsDir, d, "SKILL.md");
|
|
28
28
|
let description = "";
|
|
29
29
|
try {
|
|
30
30
|
const content = readFileSync(path, "utf-8");
|
|
@@ -44,18 +44,19 @@ export function registerSkillCommands(program) {
|
|
|
44
44
|
.command("load <name>")
|
|
45
45
|
.description("Load and print a skill's SKILL.md content to stdout")
|
|
46
46
|
.action((name) => {
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
const skillsDir = resolveSkillsPath();
|
|
48
|
+
const skillPath = getSkillPath(skillsDir, name);
|
|
49
|
+
if (!existsSync(skillsDir)) {
|
|
50
|
+
console.error(`Error: Skills directory not found at ${skillsDir}\n` +
|
|
50
51
|
`Run: npm install -g @9000ai/cli`);
|
|
51
52
|
process.exit(1);
|
|
52
53
|
}
|
|
53
54
|
if (!existsSync(skillPath)) {
|
|
54
55
|
// Try fuzzy match
|
|
55
|
-
const available = readdirSync(
|
|
56
|
+
const available = readdirSync(skillsDir).filter((d) => existsSync(join(skillsDir, d, "SKILL.md")));
|
|
56
57
|
const matches = available.filter((d) => d.toLowerCase().includes(name.toLowerCase()));
|
|
57
58
|
if (matches.length === 1) {
|
|
58
|
-
const realPath = getSkillPath(matches[0]);
|
|
59
|
+
const realPath = getSkillPath(skillsDir, matches[0]);
|
|
59
60
|
const content = readFileSync(realPath, "utf-8");
|
|
60
61
|
console.log(content);
|
|
61
62
|
return;
|
|
@@ -79,11 +80,12 @@ export function registerSkillCommands(program) {
|
|
|
79
80
|
.command("path [name]")
|
|
80
81
|
.description("Show skill directory path. No arg = skills root, name = specific skill path")
|
|
81
82
|
.action((name) => {
|
|
83
|
+
const skillsDir = resolveSkillsPath();
|
|
82
84
|
if (name) {
|
|
83
|
-
console.log(getSkillPath(name));
|
|
85
|
+
console.log(getSkillPath(skillsDir, name));
|
|
84
86
|
}
|
|
85
87
|
else {
|
|
86
|
-
console.log(
|
|
88
|
+
console.log(skillsDir);
|
|
87
89
|
}
|
|
88
90
|
});
|
|
89
91
|
}
|
package/dist/commands/update.js
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, cpSync, readFileSync, readdirSync, statSync } from "fs";
|
|
2
2
|
import { dirname, join, relative } from "path";
|
|
3
|
-
import { homedir } from "os";
|
|
4
3
|
import { fileURLToPath } from "url";
|
|
4
|
+
import { resolveSkillsPath } from "../config.js";
|
|
5
5
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6
6
|
export function registerUpdateCommand(program) {
|
|
7
7
|
program
|
|
8
8
|
.command("update")
|
|
9
9
|
.description("Sync skills to latest version after npm update")
|
|
10
|
-
.
|
|
10
|
+
.option("--path <dir>", "Target skills directory (one-time, does not save to config)")
|
|
11
|
+
.action((opts) => {
|
|
11
12
|
const src = join(__dirname, "..", "skills");
|
|
12
|
-
const dest =
|
|
13
|
+
const dest = resolveSkillsPath(opts.path);
|
|
13
14
|
if (!existsSync(src)) {
|
|
14
15
|
console.error("Error: Skills not found in package. Reinstall: npm install -g @9000ai/cli");
|
|
15
16
|
process.exit(1);
|
package/dist/config.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
export interface AppConfig {
|
|
2
2
|
base_url?: string;
|
|
3
3
|
api_key?: string;
|
|
4
|
+
skills_path?: string;
|
|
4
5
|
}
|
|
5
6
|
export declare function loadConfig(): AppConfig;
|
|
6
7
|
export declare function saveConfig(patch: Partial<AppConfig>): void;
|
|
7
8
|
export declare function resolveBaseUrl(override?: string): string;
|
|
8
9
|
export declare function resolveApiKey(override?: string): string;
|
|
10
|
+
export declare function resolveSkillsPath(override?: string): string;
|
package/dist/config.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
4
|
const CONFIG_DIR = join(homedir(), ".9000ai");
|
|
5
5
|
const CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
6
6
|
export function loadConfig() {
|
|
@@ -35,3 +35,14 @@ export function resolveApiKey(override) {
|
|
|
35
35
|
}
|
|
36
36
|
return key;
|
|
37
37
|
}
|
|
38
|
+
export function resolveSkillsPath(override) {
|
|
39
|
+
if (override)
|
|
40
|
+
return resolve(override);
|
|
41
|
+
const env = process.env["9000AI_SKILLS_PATH"];
|
|
42
|
+
if (env)
|
|
43
|
+
return resolve(env);
|
|
44
|
+
const cfg = loadConfig();
|
|
45
|
+
if (cfg.skills_path)
|
|
46
|
+
return resolve(cfg.skills_path);
|
|
47
|
+
return join(homedir(), ".9000ai", "skills");
|
|
48
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -14,7 +14,7 @@ const program = new Command();
|
|
|
14
14
|
program
|
|
15
15
|
.name("9000ai")
|
|
16
16
|
.description("9000AI Toolbox CLI — unified interface for 9000AI platform")
|
|
17
|
-
.version("0.5.
|
|
17
|
+
.version("0.5.5");
|
|
18
18
|
registerConfigCommands(program);
|
|
19
19
|
registerAuthCommands(program);
|
|
20
20
|
registerSearchCommands(program);
|
package/dist/output.d.ts
CHANGED
|
@@ -3,4 +3,5 @@ export declare function writeJson(filename: string, data: unknown): string;
|
|
|
3
3
|
export declare function writeTsv(filename: string, rows: Record<string, unknown>[]): string;
|
|
4
4
|
export declare function listOutputFiles(): string[];
|
|
5
5
|
export declare function readOutputJson(nameOrPath: string): unknown;
|
|
6
|
+
export declare function formatCsv(rows: Record<string, unknown>[], keys: string[]): string;
|
|
6
7
|
export declare function timestampSlug(): string;
|
package/dist/output.js
CHANGED
|
@@ -42,6 +42,16 @@ export function readOutputJson(nameOrPath) {
|
|
|
42
42
|
}
|
|
43
43
|
return JSON.parse(readFileSync(filepath, "utf-8"));
|
|
44
44
|
}
|
|
45
|
+
export function formatCsv(rows, keys) {
|
|
46
|
+
const header = keys.join(",");
|
|
47
|
+
const lines = rows.map((row) => keys.map((k) => {
|
|
48
|
+
const v = String(row[k] ?? "");
|
|
49
|
+
return v.includes(",") || v.includes('"') || v.includes("\n")
|
|
50
|
+
? `"${v.replace(/"/g, '""')}"`
|
|
51
|
+
: v;
|
|
52
|
+
}).join(","));
|
|
53
|
+
return [header, ...lines].join("\n");
|
|
54
|
+
}
|
|
45
55
|
export function timestampSlug() {
|
|
46
56
|
const now = new Date();
|
|
47
57
|
const pad = (n) => String(n).padStart(2, "0");
|
package/dist/postinstall.js
CHANGED
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { existsSync, mkdirSync, cpSync, readFileSync, readdirSync, statSync } from "fs";
|
|
6
6
|
import { dirname, join, relative } from "path";
|
|
7
|
-
import { homedir } from "os";
|
|
8
7
|
import { fileURLToPath } from "url";
|
|
8
|
+
import { resolveSkillsPath } from "./config.js";
|
|
9
9
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
10
|
const src = join(__dirname, "..", "skills");
|
|
11
|
-
const dest =
|
|
11
|
+
const dest = resolveSkillsPath();
|
|
12
12
|
// Read version
|
|
13
13
|
let version = "unknown";
|
|
14
14
|
try {
|
package/package.json
CHANGED
|
@@ -25,10 +25,11 @@ output-format: routing-only
|
|
|
25
25
|
2. **异步任务用 `--wait`。** 搜索和转写加 `--wait`,一条命令拿到最终结果
|
|
26
26
|
3. **有依赖的操作串行。** 上一步输出是下一步输入时,必须等
|
|
27
27
|
4. **用 `--fields` 只取需要的字段。** 永远不要全量读取
|
|
28
|
+
5. **列表数据用 `--format csv`。** 比 JSON 省 token,适合视频列表、搜索结果等
|
|
28
29
|
|
|
29
30
|
```bash
|
|
30
31
|
# 单任务
|
|
31
|
-
9000ai search keyword "美食探店" --wait --fields desc,likes,author_name
|
|
32
|
+
9000ai search keyword "美食探店" --wait --format csv --fields desc,likes,author_name
|
|
32
33
|
|
|
33
34
|
# 多数据源并行
|
|
34
35
|
并行 {
|
|
@@ -99,7 +99,7 @@ output-format: task-oriented
|
|
|
99
99
|
9000ai config set --base-url http://127.0.0.1:8025 --api-key <key>
|
|
100
100
|
9000ai auth whoami
|
|
101
101
|
9000ai auth capabilities
|
|
102
|
-
9000ai monitor fetch --sec-user <sec_user_id> --fields
|
|
102
|
+
9000ai monitor fetch --sec-user <sec_user_id> --fields desc,likes,comments,plays
|
|
103
103
|
9000ai monitor list-creators
|
|
104
104
|
9000ai monitor create-creator --json-file creator.json
|
|
105
105
|
9000ai monitor update-creator --creator-id <creator_id> --json-file creator_update.json
|
|
@@ -118,7 +118,7 @@ output-format: task-oriented
|
|
|
118
118
|
|
|
119
119
|
### 数据精简
|
|
120
120
|
|
|
121
|
-
任务结果和监控详情都支持 `--fields` 和 `--
|
|
121
|
+
任务结果和监控详情都支持 `--fields`、`--compact` 和 `--format csv`。**默认必须用 `--fields` 只取需要的字段,禁止全量读取。**
|
|
122
122
|
|
|
123
123
|
```bash
|
|
124
124
|
# 错误 — 返回全量 JSON,浪费上下文
|
|
@@ -127,7 +127,10 @@ output-format: task-oriented
|
|
|
127
127
|
# 正确 — 只取监控摘要字段
|
|
128
128
|
9000ai task results --task-id <id> --fields desc,author_name,likes,comments,create_time
|
|
129
129
|
|
|
130
|
-
#
|
|
130
|
+
# CSV 格式 — 省 token,适合列表数据
|
|
131
|
+
9000ai monitor fetch --sec-user <id> --format csv --fields desc,likes,shares,create_time
|
|
132
|
+
|
|
133
|
+
# 更紧凑 — 每条一行 JSON
|
|
131
134
|
9000ai task results --task-id <id> --fields desc,author_name,likes --compact
|
|
132
135
|
```
|
|
133
136
|
|
|
@@ -145,7 +145,7 @@ output/
|
|
|
145
145
|
|
|
146
146
|
### 数据精简
|
|
147
147
|
|
|
148
|
-
搜索结果和任务结果都支持 `--fields` 和 `--
|
|
148
|
+
搜索结果和任务结果都支持 `--fields`、`--compact` 和 `--format csv`。**默认必须用 `--fields` 只取需要的字段,禁止全量读取。**
|
|
149
149
|
|
|
150
150
|
```bash
|
|
151
151
|
# 错误 — 返回全量 JSON,浪费上下文
|
|
@@ -154,7 +154,10 @@ output/
|
|
|
154
154
|
# 正确 — 只取选题需要的字段
|
|
155
155
|
9000ai search batch-result --batch-id <id> --fields desc,author_name,likes,duration
|
|
156
156
|
|
|
157
|
-
#
|
|
157
|
+
# CSV 格式 — 省 token,适合列表数据
|
|
158
|
+
9000ai search keyword "美食探店" --wait --format csv --fields desc,author_name,author_sec_uid,likes
|
|
159
|
+
|
|
160
|
+
# 更紧凑 — 每条一行 JSON
|
|
158
161
|
9000ai search batch-result --batch-id <id> --fields desc,author_name,likes --compact
|
|
159
162
|
```
|
|
160
163
|
|