@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,257 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { EXIT, UsageError } from './errors.js';
|
|
3
|
+
import { print, printJson } from './output.js';
|
|
4
|
+
import { clientFromConfig } from './commands/context.js';
|
|
5
|
+
const URN_PATTERN = /^cortex:\/\/kb\/[^\s:]+$/;
|
|
6
|
+
const DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
|
|
7
|
+
const DIGITS_PATTERN = /^\d+$/;
|
|
8
|
+
const ITEMS_SAMPLE_HINT = "示例:--items '[{\"target\":\"cortex://kb/KBxxxx/doc/NDxxxx:10-60\"}]'";
|
|
9
|
+
export function buildToolArgs(spec, context) {
|
|
10
|
+
const args = spec.buildArgs ? spec.buildArgs(context) : buildDeclaredArgs(spec, context);
|
|
11
|
+
spec.validateArgs?.(args);
|
|
12
|
+
return args;
|
|
13
|
+
}
|
|
14
|
+
function buildDeclaredArgs(spec, context) {
|
|
15
|
+
const declared = spec.positionals ?? [];
|
|
16
|
+
if (context.positionals.length < declared.length) {
|
|
17
|
+
throw new UsageError(`缺少参数 <${declared[context.positionals.length].name}>`, `用法: ${buildUsageForSpec(spec)}`);
|
|
18
|
+
}
|
|
19
|
+
const args = {};
|
|
20
|
+
declared.forEach((positional, index) => {
|
|
21
|
+
if (positional.variadic) {
|
|
22
|
+
const rest = context.positionals.slice(index);
|
|
23
|
+
args[positional.param] = positional.kind === 'space-join' ? rest.join(' ') : JSON.stringify(rest);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const value = context.positionals[index];
|
|
27
|
+
if (positional.validate === 'urn') {
|
|
28
|
+
assertUrn(positional.name, value);
|
|
29
|
+
}
|
|
30
|
+
args[positional.param] = value;
|
|
31
|
+
});
|
|
32
|
+
for (const flag of spec.flags ?? []) {
|
|
33
|
+
const raw = context.flags[flag.flag];
|
|
34
|
+
if (raw === undefined || raw === false) {
|
|
35
|
+
if (flag.required) {
|
|
36
|
+
throw new UsageError(`缺少 --${flag.flag}`, `用法: ${buildUsageForSpec(spec)}`);
|
|
37
|
+
}
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const values = Array.isArray(raw) ? raw : [raw];
|
|
41
|
+
const mapped = values.map((value) => {
|
|
42
|
+
if (flag.kind === 'number') {
|
|
43
|
+
return toNumber(value, flag.flag);
|
|
44
|
+
}
|
|
45
|
+
return flag.kind === 'boolean' ? value === true : String(value);
|
|
46
|
+
});
|
|
47
|
+
mapped.forEach((value) => assertFlagValue(flag, value));
|
|
48
|
+
args[flag.param] = flag.multiple ? JSON.stringify(mapped) : mapped[0];
|
|
49
|
+
}
|
|
50
|
+
return args;
|
|
51
|
+
}
|
|
52
|
+
export function assertUrn(name, value) {
|
|
53
|
+
if (!URN_PATTERN.test(value)) {
|
|
54
|
+
throw new UsageError(`${name} 不是合法 URN:${value}`, 'URN 格式 cortex://kb/{kb_code}/doc/{node_code}(库级为 cortex://kb/{kb_code}),'
|
|
55
|
+
+ '可用 `yos retrieve search` 或 `yos docs tree <kb_code>` 取得');
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
export function assertDateRange(start, end, startFlag, endFlag) {
|
|
59
|
+
for (const [value, flag] of [[start, startFlag], [end, endFlag]]) {
|
|
60
|
+
if (typeof value === 'string' && !DATE_PATTERN.test(value)) {
|
|
61
|
+
throw new UsageError(`--${flag} 日期格式应为 yyyy-MM-dd:${value}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (typeof start === 'string' && typeof end === 'string' && start > end) {
|
|
65
|
+
throw new UsageError(`日期区间非法:--${startFlag} 晚于 --${endFlag}`, `起 ${start} 应不晚于止 ${end}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
export function assertTextLength(value, max, label) {
|
|
69
|
+
if (typeof value !== 'string' || value.length <= max) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
throw new UsageError(`${label}超长(实际 ${value.length} 字,上限 ${max} 字)`);
|
|
73
|
+
}
|
|
74
|
+
export function assertNotBlank(value, label, hint) {
|
|
75
|
+
if (typeof value === 'string' && value.trim().length > 0) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
throw new UsageError(`${label}不能为空`, hint);
|
|
79
|
+
}
|
|
80
|
+
export function assertAnyParam(args, params, labels) {
|
|
81
|
+
if (params.some((param) => args[param] !== undefined && args[param] !== '')) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
throw new UsageError(`${labels.join('、')} 至少传一个`);
|
|
85
|
+
}
|
|
86
|
+
export function assertGrepOrder(target, pattern) {
|
|
87
|
+
const first = String(target ?? '');
|
|
88
|
+
const second = String(pattern ?? '');
|
|
89
|
+
if (!URN_PATTERN.test(first) && URN_PATTERN.test(second)) {
|
|
90
|
+
throw new UsageError('参数顺序疑似颠倒:第 1 个参数不是 URN', 'retrieve grep 的顺序为 <target> <pattern>,与 Unix grep 相反');
|
|
91
|
+
}
|
|
92
|
+
assertUrn('target', first);
|
|
93
|
+
}
|
|
94
|
+
export function assertNumericId(value, label, hint) {
|
|
95
|
+
const text = String(value ?? '');
|
|
96
|
+
if (!DIGITS_PATTERN.test(text)) {
|
|
97
|
+
throw new UsageError(`${label} 需为数字,实际收到:${text}`, hint);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function assertFlagValue(flag, value) {
|
|
101
|
+
if (flag.values !== undefined && !flag.values.includes(String(value))) {
|
|
102
|
+
throw new UsageError(`--${flag.flag} 取值非法:${String(value)}`, `可选:${flag.values.join(' / ')}`);
|
|
103
|
+
}
|
|
104
|
+
if (typeof value !== 'number') {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (flag.min !== undefined && value < flag.min) {
|
|
108
|
+
throw new UsageError(`--${flag.flag} 低于下限 ${flag.min}:${value}`, rangeHint(flag));
|
|
109
|
+
}
|
|
110
|
+
if (flag.max !== undefined && value > flag.max) {
|
|
111
|
+
throw new UsageError(`--${flag.flag} 超过上限 ${flag.max}:${value}`, rangeHint(flag));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function rangeHint(flag) {
|
|
115
|
+
return `取值范围 ${flag.min ?? '不限'}~${flag.max ?? '不限'}`;
|
|
116
|
+
}
|
|
117
|
+
export async function runDomainCommand(spec, context) {
|
|
118
|
+
const args = buildToolArgs(spec, context);
|
|
119
|
+
const result = await clientFromConfig().callTool(spec.tool, args);
|
|
120
|
+
if (context.flags.json) {
|
|
121
|
+
printJson(result);
|
|
122
|
+
}
|
|
123
|
+
else if (spec.render && !result.isError) {
|
|
124
|
+
spec.render(result);
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
print(result.text);
|
|
128
|
+
}
|
|
129
|
+
return result.isError ? EXIT.BUSINESS : EXIT.OK;
|
|
130
|
+
}
|
|
131
|
+
export function buildUsageForSpec(spec) {
|
|
132
|
+
const parts = [`yos ${spec.name}`];
|
|
133
|
+
for (const positional of spec.positionals ?? []) {
|
|
134
|
+
parts.push(positional.variadic ? `<${positional.name}...>` : `<${positional.name}>`);
|
|
135
|
+
}
|
|
136
|
+
for (const flag of spec.flags ?? []) {
|
|
137
|
+
if (flag.kind === 'boolean') {
|
|
138
|
+
parts.push(`[--${flag.flag}]`);
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
parts.push(flag.required ? `--${flag.flag} <值>` : `[--${flag.flag} <值>]`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
parts.push('[--json]');
|
|
145
|
+
return parts.join(' ');
|
|
146
|
+
}
|
|
147
|
+
export function buildOptionsForSpec(spec) {
|
|
148
|
+
const options = {
|
|
149
|
+
json: { type: 'boolean' },
|
|
150
|
+
};
|
|
151
|
+
for (const flag of spec.flags ?? []) {
|
|
152
|
+
const option = {
|
|
153
|
+
type: flag.kind === 'boolean' ? 'boolean' : 'string',
|
|
154
|
+
};
|
|
155
|
+
if (flag.multiple) {
|
|
156
|
+
option.multiple = true;
|
|
157
|
+
}
|
|
158
|
+
options[flag.flag] = option;
|
|
159
|
+
}
|
|
160
|
+
return options;
|
|
161
|
+
}
|
|
162
|
+
export function buildParamNotesForSpec(spec) {
|
|
163
|
+
const notes = [];
|
|
164
|
+
for (const positional of spec.positionals ?? []) {
|
|
165
|
+
if (positional.description !== undefined) {
|
|
166
|
+
notes.push([positional.variadic ? `<${positional.name}...>` : `<${positional.name}>`, positional.description]);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
for (const flag of spec.flags ?? []) {
|
|
170
|
+
notes.push([`--${flag.flag}`, `${flag.description}${flag.required ? '(必填)' : ''}`]);
|
|
171
|
+
}
|
|
172
|
+
notes.push(['--json', '结构化输出(工具返回原文在 text 字段内)']);
|
|
173
|
+
return notes;
|
|
174
|
+
}
|
|
175
|
+
export function buildParamAliasForSpec(spec) {
|
|
176
|
+
const alias = {};
|
|
177
|
+
for (const flag of spec.flags ?? []) {
|
|
178
|
+
if (flag.param !== flag.flag) {
|
|
179
|
+
alias[flag.param] = flag.flag;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return alias;
|
|
183
|
+
}
|
|
184
|
+
export function parseReadTarget(raw) {
|
|
185
|
+
const matched = /:(\d+)-(\d+)$/.exec(raw);
|
|
186
|
+
if (!matched) {
|
|
187
|
+
assertUrn('target', raw);
|
|
188
|
+
return { target: raw, offset: 1, limit: 200 };
|
|
189
|
+
}
|
|
190
|
+
const offset = Number(matched[1]);
|
|
191
|
+
const end = Number(matched[2]);
|
|
192
|
+
if (offset < 1) {
|
|
193
|
+
throw new UsageError(`行号区间非法(起 < 1):${raw}`, '行号从 1 开始,区间格式 <target>:起-止');
|
|
194
|
+
}
|
|
195
|
+
if (end < offset) {
|
|
196
|
+
throw new UsageError(`行号区间非法(止 < 起):${raw}`, '区间格式 <target>:起-止,如 cortex://kb/KB1/doc/ND1:10-50');
|
|
197
|
+
}
|
|
198
|
+
const target = raw.slice(0, matched.index);
|
|
199
|
+
assertUrn('target', target);
|
|
200
|
+
return { target, offset, limit: end - offset + 1 };
|
|
201
|
+
}
|
|
202
|
+
export function normalizeReadItems(raw) {
|
|
203
|
+
let parsed;
|
|
204
|
+
try {
|
|
205
|
+
parsed = JSON.parse(raw);
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
throw new UsageError('--items 不是合法 JSON', ITEMS_SAMPLE_HINT);
|
|
209
|
+
}
|
|
210
|
+
if (!Array.isArray(parsed)) {
|
|
211
|
+
throw new UsageError('--items 应为 JSON 数组', ITEMS_SAMPLE_HINT);
|
|
212
|
+
}
|
|
213
|
+
return JSON.stringify(parsed.map(normalizeReadItem));
|
|
214
|
+
}
|
|
215
|
+
function normalizeReadItem(item) {
|
|
216
|
+
const record = item;
|
|
217
|
+
if (typeof item !== 'object' || item === null || typeof record.target !== 'string') {
|
|
218
|
+
throw new UsageError('--items 每项须为含 target 字符串的对象', ITEMS_SAMPLE_HINT);
|
|
219
|
+
}
|
|
220
|
+
const normalized = { ...record };
|
|
221
|
+
const parsed = parseReadTarget(record.target);
|
|
222
|
+
normalized.target = parsed.target;
|
|
223
|
+
if (parsed.target !== record.target) {
|
|
224
|
+
normalized.offset = record.offset ?? parsed.offset;
|
|
225
|
+
normalized.limit = record.limit ?? parsed.limit;
|
|
226
|
+
}
|
|
227
|
+
return normalized;
|
|
228
|
+
}
|
|
229
|
+
export function resolveContent(flags, required) {
|
|
230
|
+
const content = typeof flags.content === 'string' ? flags.content : undefined;
|
|
231
|
+
const file = typeof flags.file === 'string' ? flags.file : undefined;
|
|
232
|
+
if (content !== undefined && file !== undefined) {
|
|
233
|
+
throw new UsageError('--content 与 --file 互斥', "二选一:--content '<文本>' 或 --file <本地文件路径>");
|
|
234
|
+
}
|
|
235
|
+
if (file !== undefined) {
|
|
236
|
+
try {
|
|
237
|
+
return readFileSync(file, 'utf8');
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
throw new UsageError(`文件不存在或不可读:${file}`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (content !== undefined) {
|
|
244
|
+
return content;
|
|
245
|
+
}
|
|
246
|
+
if (required) {
|
|
247
|
+
throw new UsageError('缺少正文内容', "用 --content '<文本>' 或 --file <本地文件路径> 提供");
|
|
248
|
+
}
|
|
249
|
+
return undefined;
|
|
250
|
+
}
|
|
251
|
+
function toNumber(value, flag) {
|
|
252
|
+
const num = Number(value);
|
|
253
|
+
if (Number.isNaN(num)) {
|
|
254
|
+
throw new UsageError(`--${flag} 需要数字,实际:${String(value)}`);
|
|
255
|
+
}
|
|
256
|
+
return num;
|
|
257
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const DEFAULT_ENDPOINT = 'https://yos-bff.linkduoo.com';
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export const EXIT = {
|
|
2
|
+
OK: 0,
|
|
3
|
+
BUSINESS: 1,
|
|
4
|
+
USAGE: 2,
|
|
5
|
+
NETWORK: 3,
|
|
6
|
+
AUTH: 4,
|
|
7
|
+
};
|
|
8
|
+
export class CliError extends Error {
|
|
9
|
+
exitCode;
|
|
10
|
+
hint;
|
|
11
|
+
constructor(message, exitCode, hint) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.exitCode = exitCode;
|
|
14
|
+
this.hint = hint;
|
|
15
|
+
this.name = new.target.name;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export class UsageError extends CliError {
|
|
19
|
+
constructor(message, hint) {
|
|
20
|
+
super(message, EXIT.USAGE, hint);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export class BusinessError extends CliError {
|
|
24
|
+
constructor(message, hint) {
|
|
25
|
+
super(message, EXIT.BUSINESS, hint);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export class NetworkError extends CliError {
|
|
29
|
+
constructor(message, hint) {
|
|
30
|
+
super(message, EXIT.NETWORK, hint);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export class AuthError extends CliError {
|
|
34
|
+
constructor(message) {
|
|
35
|
+
super(message, EXIT.AUTH, '请执行 `yos login` 重新配置访问令牌');
|
|
36
|
+
}
|
|
37
|
+
}
|
package/dist/index.js
ADDED
package/dist/npm-meta.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, join, resolve, sep } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { promisify } from 'node:util';
|
|
6
|
+
import { NetworkError } from './errors.js';
|
|
7
|
+
const execFileAsync = promisify(execFile);
|
|
8
|
+
export const PUBLIC_REGISTRY = 'https://registry.npmjs.org';
|
|
9
|
+
export function readPackageMeta() {
|
|
10
|
+
let dir = resolve(dirname(fileURLToPath(import.meta.url)));
|
|
11
|
+
for (;;) {
|
|
12
|
+
const candidate = join(dir, 'package.json');
|
|
13
|
+
if (existsSync(candidate)) {
|
|
14
|
+
const parsed = tryParseJson(candidate);
|
|
15
|
+
if (parsed?.name && parsed?.version) {
|
|
16
|
+
return { name: parsed.name, version: parsed.version, dir };
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const parent = dirname(dir);
|
|
20
|
+
if (parent === dir) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
dir = parent;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export async function runNpm(args) {
|
|
27
|
+
const useCmdWrapper = process.platform === 'win32';
|
|
28
|
+
const command = useCmdWrapper ? (process.env.ComSpec ?? 'cmd.exe') : 'npm';
|
|
29
|
+
const commandArgs = useCmdWrapper ? ['/d', '/s', '/c', 'npm', ...args] : args;
|
|
30
|
+
try {
|
|
31
|
+
const { stdout, stderr } = await execFileAsync(command, commandArgs, {
|
|
32
|
+
encoding: 'utf8',
|
|
33
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
34
|
+
timeout: 30_000,
|
|
35
|
+
windowsHide: true,
|
|
36
|
+
});
|
|
37
|
+
return { ok: true, stdout: `${stdout}`.trim(), stderr: `${stderr}`.trim() };
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
const failure = error;
|
|
41
|
+
return {
|
|
42
|
+
ok: false,
|
|
43
|
+
stdout: `${failure.stdout ?? ''}`.trim(),
|
|
44
|
+
stderr: `${failure.stderr || failure.message || ''}`.trim(),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export async function fetchLatestVersion(name) {
|
|
49
|
+
const result = await runNpm(['view', name, 'version', `--registry=${PUBLIC_REGISTRY}`]);
|
|
50
|
+
if (!result.ok) {
|
|
51
|
+
const reason = result.stderr.split(/\r?\n/)[0]?.trim() || result.stdout.split(/\r?\n/)[0]?.trim() || '';
|
|
52
|
+
throw new NetworkError(`无法获知 ${name} 的最新版本:npm view 调用失败`, `${reason ? `${reason};` : ''}离线或 npm 不可用时,手动升级请执行 npm install -g ${name}@latest`);
|
|
53
|
+
}
|
|
54
|
+
const latest = result.stdout.split(/\r?\n/)[0]?.trim() ?? '';
|
|
55
|
+
if (!/^\d+\.\d+\.\d+/.test(latest)) {
|
|
56
|
+
throw new NetworkError(`无法解析 ${name} 的最新版本输出`, `npm view 返回:${result.stdout.slice(0, 120)}`);
|
|
57
|
+
}
|
|
58
|
+
return latest;
|
|
59
|
+
}
|
|
60
|
+
export function compareVersions(a, b) {
|
|
61
|
+
const left = toSegments(a);
|
|
62
|
+
const right = toSegments(b);
|
|
63
|
+
for (let i = 0; i < Math.max(left.length, right.length); i += 1) {
|
|
64
|
+
const diff = (left[i] ?? 0) - (right[i] ?? 0);
|
|
65
|
+
if (diff !== 0) {
|
|
66
|
+
return diff < 0 ? -1 : 1;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return 0;
|
|
70
|
+
}
|
|
71
|
+
export async function detectGlobalInstall(meta) {
|
|
72
|
+
const result = await runNpm(['prefix', '-g']);
|
|
73
|
+
if (!result.ok || result.stdout.length === 0) {
|
|
74
|
+
return { global: false, prefix: '' };
|
|
75
|
+
}
|
|
76
|
+
const prefix = stripTrailingSep(resolve(result.stdout));
|
|
77
|
+
const packageDir = stripTrailingSep(meta.dir);
|
|
78
|
+
return {
|
|
79
|
+
global: packageDir === prefix || packageDir.startsWith(`${prefix}${sep}`),
|
|
80
|
+
prefix,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function toSegments(version) {
|
|
84
|
+
return version.trim().split('-')[0].split('.').map((part) => Number.parseInt(part, 10) || 0);
|
|
85
|
+
}
|
|
86
|
+
function tryParseJson(path) {
|
|
87
|
+
try {
|
|
88
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function stripTrailingSep(path) {
|
|
95
|
+
return path.replace(/[/\\]+$/, '');
|
|
96
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { readFile, stat } from 'node:fs/promises';
|
|
2
|
+
import { basename } from 'node:path';
|
|
3
|
+
import { AuthError, BusinessError, NetworkError } from './errors.js';
|
|
4
|
+
export const MAX_UPLOAD_SIZE_BYTES = 20 * 1024 * 1024;
|
|
5
|
+
export function formatIdentity(identity) {
|
|
6
|
+
return identity.userName ? `${identity.userName}(${identity.userCode})` : identity.userCode;
|
|
7
|
+
}
|
|
8
|
+
export class OpenApisClient {
|
|
9
|
+
base;
|
|
10
|
+
apiKey;
|
|
11
|
+
constructor(base, apiKey) {
|
|
12
|
+
this.base = base;
|
|
13
|
+
this.apiKey = apiKey;
|
|
14
|
+
}
|
|
15
|
+
async whoami() {
|
|
16
|
+
return this.request('/user/whoami', {});
|
|
17
|
+
}
|
|
18
|
+
async listTools() {
|
|
19
|
+
const tools = await this.request('/tool/list', {});
|
|
20
|
+
return tools ?? [];
|
|
21
|
+
}
|
|
22
|
+
async toolSchema(name) {
|
|
23
|
+
return this.request('/tool/schema', { name });
|
|
24
|
+
}
|
|
25
|
+
async callTool(name, args) {
|
|
26
|
+
return this.request('/tool/call', { name, arguments: args });
|
|
27
|
+
}
|
|
28
|
+
async request(path, body) {
|
|
29
|
+
let response;
|
|
30
|
+
try {
|
|
31
|
+
response = await fetch(`${this.base}${path}`, {
|
|
32
|
+
method: 'POST',
|
|
33
|
+
headers: {
|
|
34
|
+
'Content-Type': 'application/json',
|
|
35
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
36
|
+
},
|
|
37
|
+
body: JSON.stringify(body ?? {}),
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
throw new NetworkError(`无法连接 YOS 服务:endpoint=${this.base},原因:${e.message}`);
|
|
42
|
+
}
|
|
43
|
+
return parseEnvelope(response);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
async function parseEnvelope(response) {
|
|
47
|
+
if (response.status === 401) {
|
|
48
|
+
let reason = '访问令牌无效或已过期';
|
|
49
|
+
try {
|
|
50
|
+
const body = await response.json();
|
|
51
|
+
if (body.error) {
|
|
52
|
+
reason = body.error;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
}
|
|
57
|
+
throw new AuthError(`鉴权失败:${reason}`);
|
|
58
|
+
}
|
|
59
|
+
if (response.status === 404) {
|
|
60
|
+
throw new NetworkError(`HTTP 404:服务端不存在该接口(${response.url})`, '确认服务端已部署开放接口通道;端点配错时用 yos login --endpoint <url> --key <key> 重新指定');
|
|
61
|
+
}
|
|
62
|
+
let payload;
|
|
63
|
+
try {
|
|
64
|
+
payload = await response.json();
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
throw new NetworkError(`服务返回无法解析的响应:HTTP ${response.status}`);
|
|
68
|
+
}
|
|
69
|
+
if (!response.ok || payload.code !== 200) {
|
|
70
|
+
throw new BusinessError(payload.message ?? `服务返回异常状态:HTTP ${response.status}`);
|
|
71
|
+
}
|
|
72
|
+
return payload.data;
|
|
73
|
+
}
|
|
74
|
+
export function parseToolJson(result) {
|
|
75
|
+
try {
|
|
76
|
+
return JSON.parse(result.text);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
throw new BusinessError(`工具返回内容不是 JSON:${result.text.slice(0, 200)}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
export async function uploadKnowledgeFile(endpoint, apiKey, filePath, kbCode, parentCode, fileName) {
|
|
83
|
+
let fileSize;
|
|
84
|
+
try {
|
|
85
|
+
fileSize = (await stat(filePath)).size;
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
throw new BusinessError(`文件不存在或不可读:path=${filePath}`);
|
|
89
|
+
}
|
|
90
|
+
if (fileSize > MAX_UPLOAD_SIZE_BYTES) {
|
|
91
|
+
throw new BusinessError(`文件超过 20MB 上限:size=${fileSize},path=${filePath}`);
|
|
92
|
+
}
|
|
93
|
+
const content = await readFile(filePath);
|
|
94
|
+
const form = new FormData();
|
|
95
|
+
form.append('file', new Blob([content]), fileName ?? basename(filePath));
|
|
96
|
+
form.append('kb_code', kbCode);
|
|
97
|
+
if (parentCode) {
|
|
98
|
+
form.append('parent_code', parentCode);
|
|
99
|
+
}
|
|
100
|
+
let response;
|
|
101
|
+
try {
|
|
102
|
+
response = await fetch(`${endpoint}/open-apis/knowledge/file_upload`, {
|
|
103
|
+
method: 'POST',
|
|
104
|
+
headers: { 'Authorization': `Bearer ${apiKey}` },
|
|
105
|
+
body: form,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
catch (e) {
|
|
109
|
+
throw new NetworkError(`无法连接 YOS 服务:endpoint=${endpoint},原因:${e.message}`);
|
|
110
|
+
}
|
|
111
|
+
return parseEnvelope(response);
|
|
112
|
+
}
|
package/dist/output.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export function print(value) {
|
|
2
|
+
process.stdout.write(`${value}\n`);
|
|
3
|
+
}
|
|
4
|
+
export function printJson(value) {
|
|
5
|
+
process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
|
|
6
|
+
}
|
|
7
|
+
export function printErr(value) {
|
|
8
|
+
process.stderr.write(`${value}\n`);
|
|
9
|
+
}
|
|
10
|
+
export function renderRows(rows) {
|
|
11
|
+
const width = Math.max(...rows.map(([left]) => left.length), 0);
|
|
12
|
+
return rows.map(([left, right]) => `${left.padEnd(width)} ${right}`).join('\n');
|
|
13
|
+
}
|
|
14
|
+
export function summarize(text, maxChars = 40) {
|
|
15
|
+
const firstLine = text.split('\n')[0].trim();
|
|
16
|
+
if (firstLine.length === 0) {
|
|
17
|
+
return '';
|
|
18
|
+
}
|
|
19
|
+
const sentence = firstLine.split('。')[0];
|
|
20
|
+
return sentence.length <= maxChars ? sentence : `${sentence.slice(0, maxChars)}…`;
|
|
21
|
+
}
|
package/dist/prompt.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export async function readHiddenLine(question) {
|
|
2
|
+
process.stderr.write(question);
|
|
3
|
+
if (!process.stdin.isTTY) {
|
|
4
|
+
const chunks = [];
|
|
5
|
+
for await (const chunk of process.stdin) {
|
|
6
|
+
chunks.push(chunk);
|
|
7
|
+
if (chunk.includes(10)) {
|
|
8
|
+
break;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
return Buffer.concat(chunks).toString('utf8').split('\n')[0].trim();
|
|
12
|
+
}
|
|
13
|
+
return new Promise((resolve) => {
|
|
14
|
+
process.stdin.setRawMode(true);
|
|
15
|
+
process.stdin.resume();
|
|
16
|
+
process.stdin.setEncoding('utf8');
|
|
17
|
+
let value = '';
|
|
18
|
+
const onData = (chunk) => {
|
|
19
|
+
for (const ch of chunk) {
|
|
20
|
+
if (ch === '\r' || ch === '\n') {
|
|
21
|
+
process.stdin.setRawMode(false);
|
|
22
|
+
process.stdin.pause();
|
|
23
|
+
process.stdin.off('data', onData);
|
|
24
|
+
process.stderr.write('\n');
|
|
25
|
+
resolve(value.trim());
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (ch === '\u0003') {
|
|
29
|
+
process.exit(130);
|
|
30
|
+
}
|
|
31
|
+
if (ch === '\u007f') {
|
|
32
|
+
value = value.slice(0, -1);
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
value += ch;
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
process.stdin.on('data', onData);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { cpSync, existsSync, mkdirSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { readPackageMeta } from './npm-meta.js';
|
|
6
|
+
import { updateConfig } from './config.js';
|
|
7
|
+
import { saveVersionState } from './version-state.js';
|
|
8
|
+
export const BUILTIN_SKILLS = ['yos', 'yos-shared', 'yos-kb', 'yos-retrieval', 'yos-transcript', 'yos-support', 'yos-install'];
|
|
9
|
+
const AGENT_SKILL_DIRS = ['.claude/skills', '.workbuddy/skills', '.cursor/skills', '.codex/skills', '.qoder/skills', '.qoder-cn/skills', '.agentskill'];
|
|
10
|
+
export function skillsSourceDir() {
|
|
11
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
const distCandidate = join(here, 'skills');
|
|
13
|
+
return existsSync(distCandidate) ? distCandidate : join(here, '..', 'src', 'skills');
|
|
14
|
+
}
|
|
15
|
+
export function detectSkillTargets() {
|
|
16
|
+
return AGENT_SKILL_DIRS
|
|
17
|
+
.map((rel) => join(homedir(), rel))
|
|
18
|
+
.filter((dir) => existsSync(dir));
|
|
19
|
+
}
|
|
20
|
+
export function installSkillsTo(targets) {
|
|
21
|
+
const source = skillsSourceDir();
|
|
22
|
+
for (const target of targets) {
|
|
23
|
+
for (const skill of BUILTIN_SKILLS) {
|
|
24
|
+
const skillTarget = join(target, skill);
|
|
25
|
+
mkdirSync(skillTarget, { recursive: true });
|
|
26
|
+
cpSync(join(source, skill), skillTarget, { recursive: true });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
updateConfig({ installedSkillDirs: targets });
|
|
30
|
+
const meta = readPackageMeta();
|
|
31
|
+
if (meta) {
|
|
32
|
+
saveVersionState(meta.version);
|
|
33
|
+
}
|
|
34
|
+
}
|