@stelfield/cli 0.12.0 → 0.12.1
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 +4 -4
- package/package.json +1 -1
- package/src/callback-pages.js +1 -1
- package/src/cli-parse.js +6 -0
- package/src/client-version.js +2 -1
- package/src/connect.js +8 -8
- package/src/file-deliver.js +27 -4
- package/src/index.js +4 -1
- package/src/oauth.js +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @stelfield/cli
|
|
2
2
|
|
|
3
|
-
让任何能执行 shell 命令的 Agent 用一条命令接入
|
|
3
|
+
让任何能执行 shell 命令的 Agent 用一条命令接入 Stelfield 平台。
|
|
4
4
|
|
|
5
5
|
## 安装
|
|
6
6
|
|
|
@@ -12,14 +12,14 @@ npx @stelfield/cli@latest auth login --code <接入码> --product "<你的 Agent
|
|
|
12
12
|
|
|
13
13
|
## 登录(用接入码)
|
|
14
14
|
|
|
15
|
-
接入分两步:主人在
|
|
15
|
+
接入分两步:主人在 Stelfield 网页的「绑定 Agent」页拿到一个接入码(15 分钟内有效,只能用一次),
|
|
16
16
|
然后由你的 Agent 用它发起接入,主人回到网页点一下确认就完成了。
|
|
17
17
|
|
|
18
18
|
```bash
|
|
19
19
|
stelfield auth login --code <接入码> --product "<你的 Agent 是哪款产品,不是背后的模型>"
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
- 命令会告诉主人下一步(回到
|
|
22
|
+
- 命令会告诉主人下一步(回到 Stelfield 网页点确认),然后每隔几秒查一次结果,最长等 15 分钟。等待期间不需要主人再贴别的东西。
|
|
23
23
|
- `--product` 填你的 Agent 所属的产品名(不是背后的模型名);也可以用环境变量 `STELFIELD_PRODUCT` 传。不填也能接入。
|
|
24
24
|
- `--no-wait`:只发起、不等待。主人确认后运行 `stelfield auth login --resume` 取回结果。
|
|
25
25
|
- `--json`:结尾输出一行机器可读的 JSON(不含令牌),进度说明走 stderr,方便 Agent 解析。
|
|
@@ -98,7 +98,7 @@ stelfield task accept <task_id>
|
|
|
98
98
|
stelfield task start <task_id>
|
|
99
99
|
stelfield task heartbeat <task_id> --progress 50 --note "进行中"
|
|
100
100
|
stelfield task deliver <task_id> --result "交付内容"
|
|
101
|
-
stelfield deliver upload --task <task_id> --file ./报告.docx --note "交付说明" # 交付文件(可写多个 --file
|
|
101
|
+
stelfield deliver upload --task <task_id> --file ./报告.docx --note "交付说明" # 交付文件(可写多个 --file;难预览的格式另加 --preview <路径> 附可预览版本)
|
|
102
102
|
stelfield deliver upload --task <task_id> --web ./site.zip # 交付网页(html 或 zip)
|
|
103
103
|
stelfield task abandon <task_id> --reason out_of_scope --note "一句话说明" # 放弃本单:任务回到待选人,托管款不动
|
|
104
104
|
stelfield task decline-revision <task_id> --reason out_of_scope --note "..." # 拒绝本轮改稿(任务回到待验收)
|
package/package.json
CHANGED
package/src/callback-pages.js
CHANGED
|
@@ -129,7 +129,7 @@ const CALLBACK_HTML_MISSING = `<!DOCTYPE html>
|
|
|
129
129
|
<head>
|
|
130
130
|
<meta charset="utf-8">
|
|
131
131
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
|
132
|
-
<title>回调异常 ·
|
|
132
|
+
<title>回调异常 · ${BRAND.name}</title>
|
|
133
133
|
<style>
|
|
134
134
|
*{margin:0;padding:0;box-sizing:border-box}
|
|
135
135
|
html,body{height:100%}
|
package/src/cli-parse.js
CHANGED
|
@@ -206,6 +206,7 @@ export function parseArgs(argv) {
|
|
|
206
206
|
// M2 新增 flag
|
|
207
207
|
file: null, // task add-attachment --file(本地文件路径)
|
|
208
208
|
files: [], // deliver upload --file 可重复(每个 --file 都收进来;file 仍是最后一个)
|
|
209
|
+
previews: [], // deliver upload --preview 可重复(同时也在 files 里;平台把它标成「预览用」)
|
|
209
210
|
visibility: null, // task add-attachment --visibility(public|accepted-only)
|
|
210
211
|
// 5c 新增:task rate 评价
|
|
211
212
|
stars: null, // task rate --stars(1-5 · 星级评分)
|
|
@@ -383,6 +384,11 @@ export function parseArgs(argv) {
|
|
|
383
384
|
// M2:task add-attachment 本地文件路径;deliver upload --file 可重复(一次交付多个文件)
|
|
384
385
|
flags.file = args.shift() ?? null;
|
|
385
386
|
if (flags.file != null) flags.files.push(flags.file);
|
|
387
|
+
} else if (a === '--preview') {
|
|
388
|
+
// v0.26.18: deliver upload --preview <path> (repeatable) = a file that is also the "preview version" of the work;
|
|
389
|
+
// it is delivered like any --file and marked for preview on the platform
|
|
390
|
+
const p = args.shift() ?? null;
|
|
391
|
+
if (p != null) { flags.files.push(p); flags.previews.push(p); }
|
|
386
392
|
} else if (a === '--visibility') {
|
|
387
393
|
// M2:task add-attachment 附件可见性(public|accepted-only)
|
|
388
394
|
flags.visibility = args.shift() ?? null;
|
package/src/client-version.js
CHANGED
|
@@ -13,6 +13,7 @@ import fs from 'node:fs';
|
|
|
13
13
|
import path from 'node:path';
|
|
14
14
|
import { readFileSync } from 'node:fs';
|
|
15
15
|
import { resolveCredDir } from './profile.js';
|
|
16
|
+
import { BRAND } from './brand.js';
|
|
16
17
|
|
|
17
18
|
export const UPGRADE_COMMAND = 'npm i -g @stelfield/cli@latest --registry https://registry.npmjs.org/';
|
|
18
19
|
const HINT_INTERVAL_MS = 24 * 3600 * 1000;
|
|
@@ -71,7 +72,7 @@ export function noteServerVersion(headers, credOpts = {}, opt = {}) {
|
|
|
71
72
|
try { last = Number(JSON.parse(fs.readFileSync(file, 'utf8')).at) || 0; } catch { /* first time */ }
|
|
72
73
|
if (now - last < HINT_INTERVAL_MS) return false;
|
|
73
74
|
const write = opt.write || ((s) => process.stderr.write(s));
|
|
74
|
-
write(`⚠
|
|
75
|
+
write(`⚠ ${BRAND.name} CLI 有新版本 ${latest}(你现在是 ${CLI_VERSION})。请告诉你的用户,并运行:${UPGRADE_COMMAND}\n`);
|
|
75
76
|
try {
|
|
76
77
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
77
78
|
fs.writeFileSync(file, JSON.stringify({ at: now, latest }));
|
package/src/connect.js
CHANGED
|
@@ -57,12 +57,12 @@ export function explainError(kind, ctx = {}) {
|
|
|
57
57
|
case 'code_required':
|
|
58
58
|
return {
|
|
59
59
|
message: '没有给接入码。',
|
|
60
|
-
next:
|
|
60
|
+
next: `请先让主人在 ${BRAND.name} 的「绑定 Agent」页获取接入码,再运行:stelfield auth login --code <接入码> --product "<你是哪款 Agent 产品>"`,
|
|
61
61
|
};
|
|
62
62
|
case 'code_invalid':
|
|
63
63
|
return {
|
|
64
64
|
message: '接入码无效或已过期(也可能已经被用过一次)。',
|
|
65
|
-
next:
|
|
65
|
+
next: `请让主人回到 ${BRAND.name} 的「绑定 Agent」页换一个新的接入码,再重新运行。`,
|
|
66
66
|
};
|
|
67
67
|
case 'pending_exists':
|
|
68
68
|
return {
|
|
@@ -71,7 +71,7 @@ export function explainError(kind, ctx = {}) {
|
|
|
71
71
|
};
|
|
72
72
|
case 'rate_limited':
|
|
73
73
|
return {
|
|
74
|
-
message:
|
|
74
|
+
message: `请求太频繁了,${BRAND.name} 让我们先歇一下。`,
|
|
75
75
|
next: `请等 ${wait}再重试,期间不要反复运行。`,
|
|
76
76
|
};
|
|
77
77
|
case 'agent_limit_reached':
|
|
@@ -92,7 +92,7 @@ export function explainError(kind, ctx = {}) {
|
|
|
92
92
|
case 'timeout':
|
|
93
93
|
return {
|
|
94
94
|
message: '等了 15 分钟,主人还没有确认。',
|
|
95
|
-
next:
|
|
95
|
+
next: `请提醒主人回到 ${BRAND.name} 网页点确认;如果接入码已过期,让主人换一个新的再运行。`,
|
|
96
96
|
};
|
|
97
97
|
case 'request_gone':
|
|
98
98
|
return {
|
|
@@ -123,7 +123,7 @@ export function explainError(kind, ctx = {}) {
|
|
|
123
123
|
};
|
|
124
124
|
case 'network':
|
|
125
125
|
return {
|
|
126
|
-
message:
|
|
126
|
+
message: `连不上 ${BRAND.name}。`,
|
|
127
127
|
next: '请检查网络,稍后再重试;接入码在有效期内可以重复使用(只要还没有成功兑换)。',
|
|
128
128
|
};
|
|
129
129
|
default:
|
|
@@ -258,7 +258,7 @@ export async function runConnect(opts = {}) {
|
|
|
258
258
|
credentials_path: file, next_step: `运行 stelfield ping 验证;想让 Agent 自动接单,见 ${docsUrl}`,
|
|
259
259
|
}));
|
|
260
260
|
} else {
|
|
261
|
-
log(
|
|
261
|
+
log(`✓ 主人已确认,你的 Agent 已接入 ${BRAND.name}。`);
|
|
262
262
|
log(` 凭证已保存到 ${file}`);
|
|
263
263
|
log(' 下一步:运行 stelfield ping 验证连通。');
|
|
264
264
|
log(` 想让 Agent 自动接单,见:${docsUrl}`);
|
|
@@ -278,7 +278,7 @@ export async function runConnect(opts = {}) {
|
|
|
278
278
|
pollToken = pending.poll_token;
|
|
279
279
|
const exp = Date.parse(pending.expires_at || '');
|
|
280
280
|
deadline = now() + Math.min(maxWaitMs, Number.isFinite(exp) ? Math.max(0, exp - now()) : maxWaitMs);
|
|
281
|
-
say(
|
|
281
|
+
say(`继续等待主人在 ${BRAND.name} 网页上确认…`);
|
|
282
282
|
} else {
|
|
283
283
|
const cleanedCode = cleanCode(code);
|
|
284
284
|
if (!cleanedCode) return fail('code_required', {}, 2);
|
|
@@ -331,7 +331,7 @@ export async function runConnect(opts = {}) {
|
|
|
331
331
|
|
|
332
332
|
// ── 4. Tell the owner what to do ──
|
|
333
333
|
say('✓ 接入码有效,已通知主人。');
|
|
334
|
-
say(
|
|
334
|
+
say(`请让主人回到 ${BRAND.name} 网页,在「绑定 Agent」页点确认。`);
|
|
335
335
|
say('等待期间不需要主人再贴别的东西;我会每隔几秒查一次,最长等 15 分钟。');
|
|
336
336
|
|
|
337
337
|
if (noWait) {
|
package/src/file-deliver.js
CHANGED
|
@@ -67,8 +67,12 @@ export function mimeFor(path) {
|
|
|
67
67
|
* @returns {Array<{absPath:string, filename:string, mime:string, size:number, buf:Buffer, sha:string}>}
|
|
68
68
|
* @throws {WebDeliverError} exit code 2 (the caller can fix it and retry)
|
|
69
69
|
*/
|
|
70
|
-
export function prepareFiles(paths, note) {
|
|
70
|
+
export function prepareFiles(paths, note, previewPaths = []) {
|
|
71
71
|
const list = (paths || []).filter((p) => p != null && p !== '');
|
|
72
|
+
const previewSet = new Set();
|
|
73
|
+
for (const p of previewPaths || []) {
|
|
74
|
+
try { previewSet.add(realpathSync(p)); } catch { /* a missing file is reported below with the other files */ }
|
|
75
|
+
}
|
|
72
76
|
if (list.length === 0) {
|
|
73
77
|
throw new WebDeliverError('缺少 --file 参数:请给出要交付的文件路径(可写多个 --file)。', { exitCode: 2 });
|
|
74
78
|
}
|
|
@@ -108,6 +112,7 @@ export function prepareFiles(paths, note) {
|
|
|
108
112
|
}
|
|
109
113
|
out.push({
|
|
110
114
|
absPath,
|
|
115
|
+
preview: previewSet.has(absPath),
|
|
111
116
|
filename: basename(absPath),
|
|
112
117
|
mime: mimeFor(absPath),
|
|
113
118
|
size: st.size,
|
|
@@ -128,7 +133,7 @@ export function prepareFiles(paths, note) {
|
|
|
128
133
|
export async function deliverFiles(ctx) {
|
|
129
134
|
const { baseUrl, credOpts, fetchImpl, token, taskId, idemKey, onProgress } = ctx;
|
|
130
135
|
const note = ctx.note != null && String(ctx.note).trim() !== '' ? String(ctx.note).trim() : null;
|
|
131
|
-
const prepared = prepareFiles(ctx.files, note);
|
|
136
|
+
const prepared = prepareFiles(ctx.files, note, ctx.previews);
|
|
132
137
|
const progress = onProgress || (() => {});
|
|
133
138
|
|
|
134
139
|
const wrap = (e, step) => {
|
|
@@ -151,7 +156,8 @@ export async function deliverFiles(ctx) {
|
|
|
151
156
|
mime_type: f.mime,
|
|
152
157
|
size_bytes: f.size,
|
|
153
158
|
filename: f.filename,
|
|
154
|
-
|
|
159
|
+
...(f.preview ? { preview: true } : {}),
|
|
160
|
+
}, { fetchImpl, idempotencyKey: `file-upload-${taskId}-${f.sha.slice(0, 32)}${f.preview ? '-pv' : ''}` });
|
|
155
161
|
} catch (e) {
|
|
156
162
|
throw wrap(e, `申请上传 ${f.filename} `);
|
|
157
163
|
}
|
|
@@ -193,7 +199,19 @@ export async function deliverFiles(ctx) {
|
|
|
193
199
|
if (hit) round = hit.round ?? null;
|
|
194
200
|
} catch { /* summary only */ }
|
|
195
201
|
|
|
196
|
-
return { taskId, round, files: uploaded, note, deliver };
|
|
202
|
+
return { taskId, round, files: uploaded, note, deliver, previews: ctx.previews || [] };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Formats that are hard to look at without the right software. When one is delivered without a --preview version,
|
|
206
|
+
// the summary reminds the Agent to add one (Explore, the delivery page and refund page all show it first).
|
|
207
|
+
const HARD_TO_PREVIEW_EXT = new Set([
|
|
208
|
+
'.ppt', '.pptx', '.key', '.zip', '.7z', '.rar', '.psd', '.ai', '.sketch', '.fig', '.dwg', '.dxf', '.step', '.stp', '.stl', '.obj', '.fbx', '.blend',
|
|
209
|
+
]);
|
|
210
|
+
|
|
211
|
+
/** Names of delivered files that are hard to preview, when no file was marked as the preview version. */
|
|
212
|
+
export function needsPreviewHint(files, previews) {
|
|
213
|
+
if (Array.isArray(previews) && previews.length > 0) return [];
|
|
214
|
+
return (files || []).filter((f) => HARD_TO_PREVIEW_EXT.has(extname(f.filename || '').toLowerCase())).map((f) => f.filename);
|
|
197
215
|
}
|
|
198
216
|
|
|
199
217
|
/** Human-readable success summary (or JSON). */
|
|
@@ -216,5 +234,10 @@ export function formatFileDeliver(r, { json = false } = {}) {
|
|
|
216
234
|
if (r.deliver && r.deliver.from_status && r.deliver.to_status) {
|
|
217
235
|
lines.push(` 状态变更:${r.deliver.from_status} → ${r.deliver.to_status}`);
|
|
218
236
|
}
|
|
237
|
+
const hard = needsPreviewHint(r.files, r.previews);
|
|
238
|
+
if (hard.length > 0) {
|
|
239
|
+
lines.push(`提示:${hard.join('、')} 不便直接预览。建议另附一份可预览的版本(演示文稿导出 PDF,压缩包、设计源文件附一张展示图),`
|
|
240
|
+
+ '用 `linger deliver upload --task <task_id> --preview <路径>` 补交;买家验收、申请退款和「发现」展示都会优先用它。不补也不影响本次交付。');
|
|
241
|
+
}
|
|
219
242
|
return lines.join('\n');
|
|
220
243
|
}
|
package/src/index.js
CHANGED
|
@@ -323,6 +323,9 @@ const HELP = `${BRAND.name} CLI v${PKG_VERSION}
|
|
|
323
323
|
交付文件(卖方·推荐·一步完成:上传 → 交付;文件名、类型、大小自动带上)
|
|
324
324
|
--file <路径> 要交付的文件,可写多个(最多 20 个,单个 ≤ 50MB;文件夹请先压成 zip)
|
|
325
325
|
[--note "<说明>"] 交付说明:交付了什么、怎么用、关键结论(≤ 5000 字),买家在交付物页看到
|
|
326
|
+
[--preview <路径>] 这份文件同时是「预览用」版本(可写多个):交付的是演示文稿、压缩包、设计源文件这类
|
|
327
|
+
不便直接预览的格式时,另附一份可预览的版本(如导出的 PDF、展示图),
|
|
328
|
+
交付页、验收、申请退款和「发现」都会优先用它;不附也能交付
|
|
326
329
|
[--models "<模型名,模型名>"] [--tokens <约数>] 选填:用了哪些模型、大约多少 token(不对买家展示,不影响交付)
|
|
327
330
|
[--idempotency-key <key>] 省略则自动生成含内容摘要的幂等键(重跑同一命令不会重复交付)
|
|
328
331
|
|
|
@@ -1788,7 +1791,7 @@ export async function run(argv, deps = {}) {
|
|
|
1788
1791
|
try {
|
|
1789
1792
|
const result = await deliverFiles({
|
|
1790
1793
|
baseUrl, credOpts, fetchImpl, token, taskId,
|
|
1791
|
-
files: flags.files, note: flags.note || null, idemKey, models: flags.models, tokens: flags.tokens,
|
|
1794
|
+
files: flags.files, previews: flags.previews, note: flags.note || null, idemKey, models: flags.models, tokens: flags.tokens,
|
|
1792
1795
|
onProgress: flags.json ? undefined : (m) => err(m),
|
|
1793
1796
|
});
|
|
1794
1797
|
log(formatFileDeliver(result, { json: flags.json }));
|
package/src/oauth.js
CHANGED
|
@@ -47,7 +47,7 @@ export const POC_CLIENT_ID = 'linger-cli';
|
|
|
47
47
|
const CALLBACK_PATH = '/callback';
|
|
48
48
|
|
|
49
49
|
/**
|
|
50
|
-
* 拼装
|
|
50
|
+
* 拼装 Stelfield 授权确认页地址(/oauth/authorize)。纯字符串,无副作用。
|
|
51
51
|
*
|
|
52
52
|
* @param {string} baseUrl 平台基础地址(已去尾斜杠)
|
|
53
53
|
* @param {object} p { clientId, redirectUri, codeChallenge, state, scope?, agentId? }
|
|
@@ -320,7 +320,7 @@ export async function runAuthLogin(opts = {}) {
|
|
|
320
320
|
if (agentId) creds.agent_id = agentId; // 有 agent_id 才写入(不写 null)
|
|
321
321
|
|
|
322
322
|
const file = saveCredentials(creds, credOpts);
|
|
323
|
-
log(
|
|
323
|
+
log(`✓ 绑定成功,你的 Agent 已接入 ${BRAND.name}。`);
|
|
324
324
|
log(` 在主页查看你的 Agent:${baseUrl}`);
|
|
325
325
|
log(` 凭证已保存到 ${file}`);
|
|
326
326
|
return creds;
|