@yosai/cli 0.0.0-stage → 0.2.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/LICENSE +201 -0
- package/README.md +66 -2
- package/dist/cli.js +188 -0
- package/dist/commands/context.js +6 -0
- package/dist/commands/docs-download.js +85 -0
- package/dist/commands/doctor.js +102 -0
- package/dist/commands/domain.js +16 -0
- package/dist/commands/index.js +10 -0
- package/dist/commands/kb-upload.js +210 -0
- package/dist/commands/kb.js +165 -0
- package/dist/commands/login.js +35 -0
- package/dist/commands/ping.js +40 -0
- package/dist/commands/skill.js +22 -0
- package/dist/commands/tool.js +129 -0
- package/dist/commands/update.js +73 -0
- package/dist/commands/version.js +44 -0
- package/dist/config.js +55 -0
- package/dist/domain-commands.js +477 -0
- package/dist/domain-executor.js +257 -0
- package/dist/endpoints.js +1 -0
- package/dist/errors.js +37 -0
- package/dist/index.js +5 -0
- package/dist/npm-meta.js +96 -0
- package/dist/openapis-client.js +112 -0
- package/dist/output.js +21 -0
- package/dist/prompt.js +40 -0
- package/dist/skill-sync.js +34 -0
- package/dist/skills/yos/SKILL.md +86 -0
- package/dist/skills/yos-install/SKILL.md +162 -0
- package/dist/skills/yos-kb/SKILL.md +388 -0
- package/dist/skills/yos-retrieval/SKILL.md +220 -0
- package/dist/skills/yos-shared/SKILL.md +185 -0
- package/dist/skills/yos-support/SKILL.md +141 -0
- package/dist/skills/yos-transcript/SKILL.md +92 -0
- package/dist/tool-domains.js +38 -0
- package/dist/version-state.js +30 -0
- package/package.json +30 -4
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { readdir } from 'node:fs/promises';
|
|
2
|
+
import { join, basename, dirname } from 'node:path';
|
|
3
|
+
export function buildPathIndex(nodes, prefix = '') {
|
|
4
|
+
const index = new Map();
|
|
5
|
+
for (const node of nodes) {
|
|
6
|
+
const path = prefix ? `${prefix}/${node.name}` : node.name;
|
|
7
|
+
index.set(path, { code: node.resourceCode, type: node.resourceType });
|
|
8
|
+
if (node.children && node.children.length > 0) {
|
|
9
|
+
for (const [childPath, value] of buildPathIndex(node.children, path)) {
|
|
10
|
+
index.set(childPath, value);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return index;
|
|
15
|
+
}
|
|
16
|
+
export function findSubtreeChildren(root, dirCode) {
|
|
17
|
+
const children = root.children ?? [];
|
|
18
|
+
if (!dirCode) {
|
|
19
|
+
return children;
|
|
20
|
+
}
|
|
21
|
+
const queue = [...children];
|
|
22
|
+
while (queue.length > 0) {
|
|
23
|
+
const node = queue.shift();
|
|
24
|
+
if (node.resourceCode === dirCode) {
|
|
25
|
+
return node.children ?? [];
|
|
26
|
+
}
|
|
27
|
+
if (node.children) {
|
|
28
|
+
queue.push(...node.children);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
export function targetDirPath(baseName, relDir) {
|
|
34
|
+
return relDir ? `${baseName}/${relDir}` : baseName;
|
|
35
|
+
}
|
|
36
|
+
export function sortDirsByDepth(dirs) {
|
|
37
|
+
return [...dirs].sort((a, b) => depthOf(a) - depthOf(b) || a.localeCompare(b));
|
|
38
|
+
}
|
|
39
|
+
function depthOf(relDir) {
|
|
40
|
+
return relDir === '' ? 0 : relDir.split('/').length;
|
|
41
|
+
}
|
|
42
|
+
export function resolveRename(name, taken) {
|
|
43
|
+
if (!taken.has(name)) {
|
|
44
|
+
return name;
|
|
45
|
+
}
|
|
46
|
+
const dot = name.lastIndexOf('.');
|
|
47
|
+
const base = dot > 0 ? name.slice(0, dot) : name;
|
|
48
|
+
const ext = dot > 0 ? name.slice(dot) : '';
|
|
49
|
+
let seq = 1;
|
|
50
|
+
while (taken.has(`${base} (${seq})${ext}`)) {
|
|
51
|
+
seq += 1;
|
|
52
|
+
}
|
|
53
|
+
return `${base} (${seq})${ext}`;
|
|
54
|
+
}
|
|
55
|
+
export function decideSingleFileUpload(name, siblingNames, onConflict) {
|
|
56
|
+
const taken = new Set(siblingNames);
|
|
57
|
+
if (!taken.has(name)) {
|
|
58
|
+
return { action: 'upload', finalName: name };
|
|
59
|
+
}
|
|
60
|
+
if (onConflict === 'skip') {
|
|
61
|
+
return { action: 'skip' };
|
|
62
|
+
}
|
|
63
|
+
if (onConflict === undefined) {
|
|
64
|
+
return { action: 'abort' };
|
|
65
|
+
}
|
|
66
|
+
return { action: 'upload', finalName: resolveRename(name, taken) };
|
|
67
|
+
}
|
|
68
|
+
export async function walkLocalDir(root) {
|
|
69
|
+
const dirs = [''];
|
|
70
|
+
const files = [];
|
|
71
|
+
await walkInto(root, '', dirs, files);
|
|
72
|
+
return { dirs, files };
|
|
73
|
+
}
|
|
74
|
+
async function walkInto(absDir, relDir, dirs, files) {
|
|
75
|
+
const entries = await readdir(absDir, { withFileTypes: true });
|
|
76
|
+
for (const entry of entries) {
|
|
77
|
+
if (entry.name.startsWith('.')) {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
const childRel = relDir ? `${relDir}/${entry.name}` : entry.name;
|
|
81
|
+
if (entry.isDirectory()) {
|
|
82
|
+
dirs.push(childRel);
|
|
83
|
+
await walkInto(join(absDir, entry.name), childRel, dirs, files);
|
|
84
|
+
}
|
|
85
|
+
else if (entry.isFile()) {
|
|
86
|
+
files.push({ relDir, name: entry.name, absPath: join(absDir, entry.name) });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
export async function uploadDirectory(deps, opts) {
|
|
91
|
+
const baseName = basename(opts.localRoot);
|
|
92
|
+
const summary = {
|
|
93
|
+
kbCode: opts.kbCode,
|
|
94
|
+
targetDir: opts.dirCode ?? null,
|
|
95
|
+
foldersCreated: [],
|
|
96
|
+
foldersReused: [],
|
|
97
|
+
uploaded: [],
|
|
98
|
+
renamed: [],
|
|
99
|
+
skipped: [],
|
|
100
|
+
failed: [],
|
|
101
|
+
conflicts: [],
|
|
102
|
+
};
|
|
103
|
+
const local = await deps.walk(opts.localRoot);
|
|
104
|
+
let index = buildPathIndex(findSubtreeChildren(await deps.fetchTree(opts.kbCode), opts.dirCode));
|
|
105
|
+
const conflicts = local.files.filter((file) => {
|
|
106
|
+
const path = `${targetDirPath(baseName, file.relDir)}/${file.name}`;
|
|
107
|
+
return index.has(path);
|
|
108
|
+
});
|
|
109
|
+
summary.conflicts = conflicts.map((file) => `${targetDirPath(baseName, file.relDir)}/${file.name}`);
|
|
110
|
+
if (conflicts.length > 0 && !opts.onConflict) {
|
|
111
|
+
return { exitCode: 1, aborted: true, summary };
|
|
112
|
+
}
|
|
113
|
+
const takenByParent = collectTakenNames(local.files, index, baseName);
|
|
114
|
+
const pending = [];
|
|
115
|
+
for (const file of local.files) {
|
|
116
|
+
const parentPath = targetDirPath(baseName, file.relDir);
|
|
117
|
+
const isConflict = index.has(`${parentPath}/${file.name}`);
|
|
118
|
+
if (!isConflict) {
|
|
119
|
+
pending.push({ file, finalName: file.name, parentPath });
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (opts.onConflict === 'skip') {
|
|
123
|
+
summary.skipped.push(`${parentPath}/${file.name}`);
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
const taken = takenByParent.get(parentPath) ?? new Set();
|
|
127
|
+
const finalName = resolveRename(file.name, taken);
|
|
128
|
+
taken.add(finalName);
|
|
129
|
+
pending.push({ file, finalName, parentPath });
|
|
130
|
+
}
|
|
131
|
+
await ensureFolders(deps, opts, baseName, local.dirs, index, summary);
|
|
132
|
+
index = buildPathIndex(findSubtreeChildren(await deps.fetchTree(opts.kbCode), opts.dirCode));
|
|
133
|
+
await uploadPendingFiles(deps, opts, pending, index, summary);
|
|
134
|
+
return { exitCode: summary.failed.length > 0 ? 1 : 0, aborted: false, summary };
|
|
135
|
+
}
|
|
136
|
+
function collectTakenNames(files, index, baseName) {
|
|
137
|
+
const taken = new Map();
|
|
138
|
+
const add = (parentPath, name) => {
|
|
139
|
+
if (!taken.has(parentPath)) {
|
|
140
|
+
taken.set(parentPath, new Set());
|
|
141
|
+
}
|
|
142
|
+
taken.get(parentPath)?.add(name);
|
|
143
|
+
};
|
|
144
|
+
for (const path of index.keys()) {
|
|
145
|
+
const parent = dirname(path);
|
|
146
|
+
add(parent, path.slice(parent.length + 1));
|
|
147
|
+
}
|
|
148
|
+
for (const file of files) {
|
|
149
|
+
add(targetDirPath(baseName, file.relDir), file.name);
|
|
150
|
+
}
|
|
151
|
+
return taken;
|
|
152
|
+
}
|
|
153
|
+
async function ensureFolders(deps, opts, baseName, dirs, index, summary) {
|
|
154
|
+
const sorted = sortDirsByDepth(dirs);
|
|
155
|
+
const maxDepth = sorted.length > 0 ? depthOf(sorted[sorted.length - 1]) : 0;
|
|
156
|
+
for (let depth = 0; depth <= maxDepth; depth += 1) {
|
|
157
|
+
const level = sorted.filter((relDir) => depthOf(relDir) === depth);
|
|
158
|
+
let createdAny = false;
|
|
159
|
+
for (const relDir of level) {
|
|
160
|
+
const path = targetDirPath(baseName, relDir);
|
|
161
|
+
if (index.get(path)?.type === 'folder') {
|
|
162
|
+
summary.foldersReused.push(path);
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
const folderName = depth === 0 ? baseName : relDir.slice(relDir.lastIndexOf('/') + 1);
|
|
166
|
+
const parentCode = depth === 0 ? opts.dirCode : index.get(dirname(path))?.code;
|
|
167
|
+
await deps.mkdir(folderName, opts.kbCode, parentCode);
|
|
168
|
+
summary.foldersCreated.push(path);
|
|
169
|
+
createdAny = true;
|
|
170
|
+
}
|
|
171
|
+
if (createdAny) {
|
|
172
|
+
replaceIndex(index, buildPathIndex(findSubtreeChildren(await deps.fetchTree(opts.kbCode), opts.dirCode)));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
function replaceIndex(target, source) {
|
|
177
|
+
target.clear();
|
|
178
|
+
for (const [key, value] of source) {
|
|
179
|
+
target.set(key, value);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
async function uploadPendingFiles(deps, opts, pending, index, summary) {
|
|
183
|
+
const total = pending.length;
|
|
184
|
+
let seq = 0;
|
|
185
|
+
for (const item of pending) {
|
|
186
|
+
seq += 1;
|
|
187
|
+
const label = `${item.parentPath}/${item.finalName}`;
|
|
188
|
+
const parentCode = index.get(item.parentPath)?.code;
|
|
189
|
+
if (!parentCode) {
|
|
190
|
+
summary.failed.push({ name: label, error: `目标文件夹未就绪:${item.parentPath}` });
|
|
191
|
+
deps.log(`[${seq}/${total}] ${label} 失败:目标文件夹未就绪`);
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
try {
|
|
195
|
+
const result = await deps.uploadFile(item.file.absPath, opts.kbCode, parentCode, item.finalName);
|
|
196
|
+
if (item.finalName !== item.file.name) {
|
|
197
|
+
summary.renamed.push({ from: item.file.name, to: item.finalName, nodeCode: result.nodeCode });
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
summary.uploaded.push({ name: item.file.name, nodeCode: result.nodeCode, fileSize: result.fileSize });
|
|
201
|
+
}
|
|
202
|
+
deps.log(`[${seq}/${total}] ${label} → ${result.nodeCode}`);
|
|
203
|
+
}
|
|
204
|
+
catch (e) {
|
|
205
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
206
|
+
summary.failed.push({ name: label, error: message });
|
|
207
|
+
deps.log(`[${seq}/${total}] ${label} 失败:${message}`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { stat } from 'node:fs/promises';
|
|
2
|
+
import { basename } from 'node:path';
|
|
3
|
+
import { register } from '../cli.js';
|
|
4
|
+
import { loadConfig, resolveEndpoint, resolveKey } from '../config.js';
|
|
5
|
+
import { BusinessError, EXIT, UsageError } from '../errors.js';
|
|
6
|
+
import { uploadKnowledgeFile } from '../openapis-client.js';
|
|
7
|
+
import { print, printErr, printJson } from '../output.js';
|
|
8
|
+
import { clientFromConfig } from './context.js';
|
|
9
|
+
import { decideSingleFileUpload, findSubtreeChildren, uploadDirectory, walkLocalDir, } from './kb-upload.js';
|
|
10
|
+
const CONFLICT_POLICIES = ['skip', 'rename'];
|
|
11
|
+
register({
|
|
12
|
+
name: 'kb upload',
|
|
13
|
+
description: '上传本地文件或整个目录到知识库(目录递归镜像文件夹,multipart 直传,单文件 ≤20MB)',
|
|
14
|
+
usage: 'yos kb upload <本地文件或目录> --kb <kb_code> [--dir <parent_code>] [--on-conflict <skip|rename>] [--json]',
|
|
15
|
+
group: '知识库 kb',
|
|
16
|
+
risk: 'write',
|
|
17
|
+
options: {
|
|
18
|
+
kb: { type: 'string' },
|
|
19
|
+
dir: { type: 'string' },
|
|
20
|
+
'on-conflict': { type: 'string' },
|
|
21
|
+
json: { type: 'boolean' },
|
|
22
|
+
},
|
|
23
|
+
paramNotes: [
|
|
24
|
+
['--kb', '知识库编码'],
|
|
25
|
+
['--dir', '目标父文件夹编码(缺省知识库根目录)'],
|
|
26
|
+
['--on-conflict', '同名冲突处理:skip 跳过 / rename 加后缀新建(缺省则中止并列出冲突)'],
|
|
27
|
+
['--json', '结构化输出(目录上传返回汇总对象)'],
|
|
28
|
+
],
|
|
29
|
+
async run({ positionals, flags }) {
|
|
30
|
+
const target = positionals[0];
|
|
31
|
+
if (!target) {
|
|
32
|
+
throw new UsageError('缺少本地文件或目录路径', '用法: yos kb upload <本地文件或目录> --kb <kb_code> [--dir <parent_code>] [--on-conflict <skip|rename>]');
|
|
33
|
+
}
|
|
34
|
+
const kbCode = flags.kb;
|
|
35
|
+
if (!kbCode) {
|
|
36
|
+
throw new UsageError('缺少 --kb 知识库编码', '可用 `yos kb list` 查看知识库编码');
|
|
37
|
+
}
|
|
38
|
+
const json = flags.json === true;
|
|
39
|
+
const onConflict = parseConflictPolicy(flags['on-conflict']);
|
|
40
|
+
const dirCode = flags.dir;
|
|
41
|
+
const isDir = (await stat(target).catch(() => {
|
|
42
|
+
throw new BusinessError(`文件或目录不存在:${target}`);
|
|
43
|
+
})).isDirectory();
|
|
44
|
+
const config = loadConfig();
|
|
45
|
+
const deps = buildDeps(resolveEndpoint(config), resolveKey(config), json);
|
|
46
|
+
return isDir
|
|
47
|
+
? uploadDir(deps, target, kbCode, dirCode, onConflict, json)
|
|
48
|
+
: uploadSingle(deps, target, kbCode, dirCode, onConflict, json);
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
function parseConflictPolicy(raw) {
|
|
52
|
+
if (raw === undefined || raw === true) {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
const value = Array.isArray(raw) ? raw[raw.length - 1] : raw;
|
|
56
|
+
if (!CONFLICT_POLICIES.includes(value)) {
|
|
57
|
+
throw new UsageError(`--on-conflict 取值非法:${value}`, '可选:skip(跳过冲突件)/ rename(加后缀新建)');
|
|
58
|
+
}
|
|
59
|
+
return value;
|
|
60
|
+
}
|
|
61
|
+
async function uploadSingle(deps, filePath, kbCode, dirCode, onConflict, json) {
|
|
62
|
+
const name = basename(filePath);
|
|
63
|
+
const siblings = findSubtreeChildren(await deps.fetchTree(kbCode), dirCode).map((node) => node.name);
|
|
64
|
+
const decision = decideSingleFileUpload(name, siblings, onConflict);
|
|
65
|
+
if (decision.action === 'abort') {
|
|
66
|
+
if (json) {
|
|
67
|
+
printJson({ aborted: true, reason: 'conflict', fileName: name });
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
printErr(`目标位置已存在同名文件,已中止(未上传):${name}`);
|
|
71
|
+
printErr('提示: 加 --on-conflict skip 跳过,或 --on-conflict rename 加后缀新建');
|
|
72
|
+
}
|
|
73
|
+
return EXIT.BUSINESS;
|
|
74
|
+
}
|
|
75
|
+
if (decision.action === 'skip') {
|
|
76
|
+
if (json) {
|
|
77
|
+
printJson({ skipped: true, fileName: name });
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
print(`已跳过(目标位置已存在同名文件):${name}`);
|
|
81
|
+
}
|
|
82
|
+
return EXIT.OK;
|
|
83
|
+
}
|
|
84
|
+
const finalName = decision.finalName;
|
|
85
|
+
try {
|
|
86
|
+
const result = await deps.uploadFile(filePath, kbCode, dirCode, finalName);
|
|
87
|
+
if (json) {
|
|
88
|
+
printJson(result);
|
|
89
|
+
}
|
|
90
|
+
else if (finalName === name) {
|
|
91
|
+
print(`已上传:${result.fileName}(${result.fileSize} 字节)→ 节点 ${result.nodeCode}`);
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
print(`已改名上传:${name} → ${result.fileName}(${result.fileSize} 字节)→ 节点 ${result.nodeCode}`);
|
|
95
|
+
}
|
|
96
|
+
return EXIT.OK;
|
|
97
|
+
}
|
|
98
|
+
catch (e) {
|
|
99
|
+
if (e instanceof BusinessError && e.message.includes('超过 20MB')) {
|
|
100
|
+
throw new BusinessError(e.message, '更大文件请先在客户端上传后共享,或拆分为多个文件');
|
|
101
|
+
}
|
|
102
|
+
throw e;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
async function uploadDir(deps, localRoot, kbCode, dirCode, onConflict, json) {
|
|
106
|
+
const { exitCode, aborted, summary } = await uploadDirectory(deps, { localRoot, kbCode, dirCode, onConflict });
|
|
107
|
+
if (aborted) {
|
|
108
|
+
if (json) {
|
|
109
|
+
printJson({ aborted: true, reason: 'conflict', ...summary });
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
printErr(`发现 ${summary.conflicts.length} 个同名冲突,已中止(未上传任何文件):`);
|
|
113
|
+
for (const path of summary.conflicts) {
|
|
114
|
+
printErr(` - ${path}`);
|
|
115
|
+
}
|
|
116
|
+
printErr('提示: 加 --on-conflict skip 跳过冲突件,或 --on-conflict rename 加后缀新建');
|
|
117
|
+
}
|
|
118
|
+
return EXIT.BUSINESS;
|
|
119
|
+
}
|
|
120
|
+
if (json) {
|
|
121
|
+
printJson(summary);
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
print(renderSummary(summary));
|
|
125
|
+
}
|
|
126
|
+
return exitCode;
|
|
127
|
+
}
|
|
128
|
+
function buildDeps(endpoint, apiKey, json) {
|
|
129
|
+
const client = clientFromConfig();
|
|
130
|
+
return {
|
|
131
|
+
async fetchTree(kbCode) {
|
|
132
|
+
const result = await client.callTool('docs_tree', { kb_code: kbCode });
|
|
133
|
+
if (result.isError) {
|
|
134
|
+
throw new BusinessError(`目录树查询失败:${result.text}`);
|
|
135
|
+
}
|
|
136
|
+
return JSON.parse(result.text);
|
|
137
|
+
},
|
|
138
|
+
async mkdir(name, kbCode, parentCode) {
|
|
139
|
+
const args = { folder_name: name, kb_code: kbCode };
|
|
140
|
+
if (parentCode) {
|
|
141
|
+
args.parent_code = parentCode;
|
|
142
|
+
}
|
|
143
|
+
const result = await client.callTool('docs_mkdir', args);
|
|
144
|
+
if (result.isError) {
|
|
145
|
+
throw new BusinessError(`创建文件夹失败:${name},${result.text}`);
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
uploadFile(absPath, kbCode, parentCode, finalName) {
|
|
149
|
+
return uploadKnowledgeFile(endpoint, apiKey, absPath, kbCode, parentCode, finalName);
|
|
150
|
+
},
|
|
151
|
+
walk: walkLocalDir,
|
|
152
|
+
log: json ? () => { } : (message) => print(message),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
function renderSummary(summary) {
|
|
156
|
+
const lines = [
|
|
157
|
+
`上传完成:新建文件夹 ${summary.foldersCreated.length}、复用文件夹 ${summary.foldersReused.length}、`
|
|
158
|
+
+ `上传 ${summary.uploaded.length}、改名 ${summary.renamed.length}、`
|
|
159
|
+
+ `跳过 ${summary.skipped.length}、失败 ${summary.failed.length}`,
|
|
160
|
+
];
|
|
161
|
+
for (const item of summary.failed) {
|
|
162
|
+
lines.push(` 失败:${item.name} — ${item.error}`);
|
|
163
|
+
}
|
|
164
|
+
return lines.join('\n');
|
|
165
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { register } from '../cli.js';
|
|
2
|
+
import { loadConfig, maskKey, normalizeEndpoint, saveConfig } from '../config.js';
|
|
3
|
+
import { DEFAULT_ENDPOINT } from '../endpoints.js';
|
|
4
|
+
import { UsageError } from '../errors.js';
|
|
5
|
+
import { formatIdentity, OpenApisClient } from '../openapis-client.js';
|
|
6
|
+
import { print } from '../output.js';
|
|
7
|
+
import { readHiddenLine } from '../prompt.js';
|
|
8
|
+
register({
|
|
9
|
+
name: 'login',
|
|
10
|
+
description: '配置访问令牌与 BFF 端点(校验身份后保存)',
|
|
11
|
+
usage: 'yos login [--endpoint <url>] [--key <key>]',
|
|
12
|
+
group: '基础',
|
|
13
|
+
options: {
|
|
14
|
+
endpoint: { type: 'string' },
|
|
15
|
+
key: { type: 'string' },
|
|
16
|
+
},
|
|
17
|
+
paramNotes: [
|
|
18
|
+
['--endpoint', 'BFF 服务地址(缺省读 YOS_ENDPOINT,再缺省生产环境;本地联调 http://localhost:8080)'],
|
|
19
|
+
['--key', '访问令牌(40 位小写 hex,缺省读 YOS_API_KEY,否则交互输入)'],
|
|
20
|
+
],
|
|
21
|
+
async run({ flags }) {
|
|
22
|
+
const config = loadConfig();
|
|
23
|
+
const endpoint = normalizeEndpoint(flags.endpoint
|
|
24
|
+
?? process.env.YOS_ENDPOINT ?? config?.endpoint ?? DEFAULT_ENDPOINT);
|
|
25
|
+
const key = flags.key
|
|
26
|
+
?? process.env.YOS_API_KEY
|
|
27
|
+
?? await readHiddenLine('请粘贴访问令牌(输入不回显,客户端「个人设置-访问令牌」页创建并复制): ');
|
|
28
|
+
if (!/^[0-9a-f]{40}$/.test(key)) {
|
|
29
|
+
throw new UsageError('令牌格式不正确(应为 40 位小写 hex)', '请在客户端「个人设置-访问令牌」页创建并复制完整令牌');
|
|
30
|
+
}
|
|
31
|
+
const identity = await new OpenApisClient(`${endpoint}/open-apis`, key).whoami();
|
|
32
|
+
saveConfig({ ...config, endpoint, apiKey: key });
|
|
33
|
+
print(`已保存配置:endpoint=${endpoint},key=${maskKey(key)},身份 ${formatIdentity(identity)}`);
|
|
34
|
+
},
|
|
35
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { register } from '../cli.js';
|
|
2
|
+
import { loadConfig, resolveEndpoint, resolveKey } from '../config.js';
|
|
3
|
+
import { CliError, EXIT } from '../errors.js';
|
|
4
|
+
import { formatIdentity, OpenApisClient } from '../openapis-client.js';
|
|
5
|
+
import { print, printErr, printJson } from '../output.js';
|
|
6
|
+
register({
|
|
7
|
+
name: 'ping',
|
|
8
|
+
description: '探活:检查服务连通与访问令牌是否有效(客户端检测对接用)',
|
|
9
|
+
usage: 'yos ping [--json]',
|
|
10
|
+
group: '基础',
|
|
11
|
+
options: { json: { type: 'boolean' } },
|
|
12
|
+
paramNotes: [['--json', '结构化输出(ok / endpoint / userCode / userName / error)']],
|
|
13
|
+
async run({ flags }) {
|
|
14
|
+
const config = loadConfig();
|
|
15
|
+
const endpoint = resolveEndpoint(config);
|
|
16
|
+
try {
|
|
17
|
+
const identity = await new OpenApisClient(`${endpoint}/open-apis`, resolveKey(config)).whoami();
|
|
18
|
+
if (flags.json) {
|
|
19
|
+
printJson({ ok: true, endpoint, userCode: identity.userCode, userName: identity.userName });
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
print(`✓ 连接成功,令牌有效:身份 ${formatIdentity(identity)}`);
|
|
23
|
+
}
|
|
24
|
+
return EXIT.OK;
|
|
25
|
+
}
|
|
26
|
+
catch (e) {
|
|
27
|
+
const failure = e instanceof CliError ? e : new CliError(String(e), EXIT.BUSINESS);
|
|
28
|
+
if (flags.json) {
|
|
29
|
+
printJson({ ok: false, endpoint, error: failure.message });
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
printErr(`错误: ${failure.message}`);
|
|
33
|
+
if (failure.hint) {
|
|
34
|
+
printErr(`提示: ${failure.hint}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return failure.exitCode;
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { register } from '../cli.js';
|
|
2
|
+
import { UsageError } from '../errors.js';
|
|
3
|
+
import { print } from '../output.js';
|
|
4
|
+
import { BUILTIN_SKILLS, detectSkillTargets, installSkillsTo } from '../skill-sync.js';
|
|
5
|
+
register({
|
|
6
|
+
name: 'skill install',
|
|
7
|
+
description: '安装内置 YOS Skill 到 Agent 技能目录',
|
|
8
|
+
usage: 'yos skill install [--target <dir>]',
|
|
9
|
+
group: 'Agent 接入',
|
|
10
|
+
options: { target: { type: 'string' } },
|
|
11
|
+
paramNotes: [['--target', '显式指定安装目标目录(缺省自动探测本机 Agent 技能目录)']],
|
|
12
|
+
async run({ flags }) {
|
|
13
|
+
const explicit = flags.target;
|
|
14
|
+
const targets = explicit ? [explicit] : detectSkillTargets();
|
|
15
|
+
if (targets.length === 0) {
|
|
16
|
+
throw new UsageError('未发现已安装的 Agent 技能目录', '请用 --target <dir> 指定(如 ~/.claude/skills)');
|
|
17
|
+
}
|
|
18
|
+
installSkillsTo(targets);
|
|
19
|
+
print(`已安装 ${BUILTIN_SKILLS.length} 个 Skill 到:\n${targets.join('\n')}`);
|
|
20
|
+
print('提示:重启 Agent 工具以确保 Skill 加载');
|
|
21
|
+
},
|
|
22
|
+
});
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { register } from '../cli.js';
|
|
2
|
+
import { EXIT, UsageError } from '../errors.js';
|
|
3
|
+
import { print, printJson, summarize } from '../output.js';
|
|
4
|
+
import { groupTools } from '../tool-domains.js';
|
|
5
|
+
import { clientFromConfig } from './context.js';
|
|
6
|
+
register({
|
|
7
|
+
name: 'tool list',
|
|
8
|
+
description: '列出 YOS 全部可用工具(按领域分组,动态获取)',
|
|
9
|
+
usage: 'yos tool list [--json]',
|
|
10
|
+
group: '工具通道',
|
|
11
|
+
options: { json: { type: 'boolean' } },
|
|
12
|
+
paramNotes: [['--json', '结构化输出']],
|
|
13
|
+
async run({ flags }) {
|
|
14
|
+
const tools = await clientFromConfig().listTools();
|
|
15
|
+
if (flags.json) {
|
|
16
|
+
printJson(tools);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
if (tools.length === 0) {
|
|
20
|
+
print('(无可用工具)');
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const nameWidth = Math.max(...tools.map((tool) => tool.name.length));
|
|
24
|
+
const sections = groupTools(tools).map((group) => {
|
|
25
|
+
const rows = group.tools
|
|
26
|
+
.map((tool) => ` ${tool.name.padEnd(nameWidth)} ${summarize(tool.description ?? '')}`)
|
|
27
|
+
.join('\n');
|
|
28
|
+
return `${group.domain}(${group.tools.length})\n${rows}`;
|
|
29
|
+
});
|
|
30
|
+
print(sections.join('\n\n'));
|
|
31
|
+
print('\n提示: 常规场景优先使用领域命令(yos kb / docs / retrieve / support / transcript --help),未覆盖的工具用 yos tool schema <工具名> 查看完整说明');
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
register({
|
|
35
|
+
name: 'tool schema',
|
|
36
|
+
description: '查看单个工具的完整说明(描述全文、入参、返参、调用示例)',
|
|
37
|
+
usage: 'yos tool schema <name> [--json]',
|
|
38
|
+
group: '工具通道',
|
|
39
|
+
options: { json: { type: 'boolean' } },
|
|
40
|
+
paramNotes: [['--json', '结构化输出']],
|
|
41
|
+
async run({ positionals, flags }) {
|
|
42
|
+
const name = resolveToolName(positionals);
|
|
43
|
+
if (!name) {
|
|
44
|
+
throw new UsageError('缺少工具名', '用法: yos tool schema <name>(先用 yos tool list 浏览可用工具)');
|
|
45
|
+
}
|
|
46
|
+
const schema = await clientFromConfig().toolSchema(name);
|
|
47
|
+
if (flags.json) {
|
|
48
|
+
printJson(schema);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
print(renderToolSchema(schema));
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
register({
|
|
55
|
+
name: 'tool call',
|
|
56
|
+
description: '调用任意 YOS 工具(参数为 JSON)',
|
|
57
|
+
usage: "yos tool call <name> [--params '<json>'] [--json]",
|
|
58
|
+
group: '工具通道',
|
|
59
|
+
options: {
|
|
60
|
+
params: { type: 'string' },
|
|
61
|
+
json: { type: 'boolean' },
|
|
62
|
+
},
|
|
63
|
+
paramNotes: [
|
|
64
|
+
['--params', "工具入参 JSON(遵循工具 schema,如 '{\"kb_code\":\"KB1\"}')"],
|
|
65
|
+
['--json', '结构化输出(工具返回原文在 text 字段内)'],
|
|
66
|
+
],
|
|
67
|
+
async run({ positionals, flags }) {
|
|
68
|
+
const name = resolveToolName(positionals);
|
|
69
|
+
if (!name) {
|
|
70
|
+
throw new UsageError('缺少工具名', "用法: yos tool call <name> [--params '<json>']");
|
|
71
|
+
}
|
|
72
|
+
const paramsRaw = flags.params ?? '{}';
|
|
73
|
+
let params;
|
|
74
|
+
try {
|
|
75
|
+
params = JSON.parse(paramsRaw);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
throw new UsageError(`--params 不是合法 JSON:${paramsRaw}`);
|
|
79
|
+
}
|
|
80
|
+
const result = await clientFromConfig().callTool(name, params);
|
|
81
|
+
if (flags.json) {
|
|
82
|
+
printJson(result);
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
print(result.text);
|
|
86
|
+
}
|
|
87
|
+
return result.isError ? EXIT.BUSINESS : EXIT.OK;
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
function resolveToolName(positionals) {
|
|
91
|
+
if (positionals.length === 0) {
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
if (!positionals.every((token) => /^[a-z][a-z0-9_-]*$/.test(token))) {
|
|
95
|
+
return positionals[0];
|
|
96
|
+
}
|
|
97
|
+
return positionals.map((token) => token.replace(/-/g, '_')).join('_');
|
|
98
|
+
}
|
|
99
|
+
function renderToolSchema(schema) {
|
|
100
|
+
const lines = [`工具: ${schema.name}`, `说明: ${schema.description ?? ''}`, ''];
|
|
101
|
+
const properties = Object.entries(schema.inputSchema?.properties ?? {});
|
|
102
|
+
const required = new Set(schema.inputSchema?.required ?? []);
|
|
103
|
+
if (properties.length === 0) {
|
|
104
|
+
lines.push('入参: 无');
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
lines.push('入参:');
|
|
108
|
+
lines.push(renderPropRows(properties.map(([key, prop]) => [
|
|
109
|
+
key,
|
|
110
|
+
String(prop?.type ?? 'any'),
|
|
111
|
+
required.has(key) ? '必填' : '可选',
|
|
112
|
+
String(prop?.description ?? '').replace(/\s*\n\s*/g, ' '),
|
|
113
|
+
])));
|
|
114
|
+
}
|
|
115
|
+
lines.push('');
|
|
116
|
+
lines.push(`返参: ${schema.outputHint || '(未登记说明)'}`);
|
|
117
|
+
lines.push('');
|
|
118
|
+
const sample = Object.fromEntries(properties.filter(([key]) => required.has(key)).map(([key]) => [key, `<${key}>`]));
|
|
119
|
+
const sampleText = Object.keys(sample).length > 0 ? ` --params '${JSON.stringify(sample)}'` : '';
|
|
120
|
+
lines.push('调用示例:');
|
|
121
|
+
lines.push(` yos tool call ${schema.name}${sampleText}`);
|
|
122
|
+
return lines.join('\n');
|
|
123
|
+
}
|
|
124
|
+
function renderPropRows(rows) {
|
|
125
|
+
const widths = [0, 1].map((index) => Math.max(...rows.map((row) => row[index].length)));
|
|
126
|
+
return rows
|
|
127
|
+
.map((row) => ` ${row[0].padEnd(widths[0])} ${row[1].padEnd(widths[1])} ${row[2]} ${row[3]}`)
|
|
128
|
+
.join('\n');
|
|
129
|
+
}
|