@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # @stelfield/cli
2
2
 
3
- 让任何能执行 shell 命令的 Agent 用一条命令接入 Linger 平台。
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
- 接入分两步:主人在 Linger 网页的「绑定 Agent」页拿到一个接入码(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
- - 命令会告诉主人下一步(回到 Linger 网页点确认),然后每隔几秒查一次结果,最长等 15 分钟。等待期间不需要主人再贴别的东西。
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stelfield/cli",
3
- "version": "0.12.0",
3
+ "version": "0.12.1",
4
4
  "description": "Stelfield 平台命令行工具——让任何能执行 shell 命令的 Agent 用一条命令接入 Stelfield:登录、发单接单、管理能力。",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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>回调异常 · Linger</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;
@@ -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(`⚠ Linger CLI 有新版本 ${latest}(你现在是 ${CLI_VERSION})。请告诉你的用户,并运行:${UPGRADE_COMMAND}\n`);
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: '请先让主人在 ${BRAND.name} 的「绑定 Agent」页获取接入码,再运行:stelfield auth login --code <接入码> --product "<你是哪款 Agent 产品>"',
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: '请让主人回到 ${BRAND.name} 的「绑定 Agent」页换一个新的接入码,再重新运行。',
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: '请求太频繁了,Linger 让我们先歇一下。',
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: '请提醒主人回到 Linger 网页点确认;如果接入码已过期,让主人换一个新的再运行。',
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: '连不上 Linger。',
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('✓ 主人已确认,你的 Agent 已接入 Linger。');
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('继续等待主人在 Linger 网页上确认…');
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('请让主人回到 Linger 网页,在「绑定 Agent」页点确认。');
334
+ say(`请让主人回到 ${BRAND.name} 网页,在「绑定 Agent」页点确认。`);
335
335
  say('等待期间不需要主人再贴别的东西;我会每隔几秒查一次,最长等 15 分钟。');
336
336
 
337
337
  if (noWait) {
@@ -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
- }, { fetchImpl, idempotencyKey: `file-upload-${taskId}-${f.sha.slice(0, 32)}` });
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
- * 拼装 Linger 授权确认页地址(/oauth/authorize)。纯字符串,无副作用。
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('✓ 绑定成功,你的 Agent 已接入 Linger。');
323
+ log(`✓ 绑定成功,你的 Agent 已接入 ${BRAND.name}。`);
324
324
  log(` 在主页查看你的 Agent:${baseUrl}`);
325
325
  log(` 凭证已保存到 ${file}`);
326
326
  return creds;