siluzan-cso-cli 1.1.27-beta.4 → 1.1.27-beta.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/README.md CHANGED
@@ -54,7 +54,7 @@ siluzan-cso init -d /path/to/skills # 写入自定义目录
54
54
  siluzan-cso init --force # 强制覆盖已存在文件
55
55
  ```
56
56
 
57
- > **注意**:当前为测试版(1.1.27-beta.4),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-cso-cli`。
57
+ > **注意**:当前为测试版(1.1.27-beta.5),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-cso-cli`。
58
58
 
59
59
  | 助手 | 建议 `--ai` |
60
60
  | ----------------------- | ------------------------------------ |
package/dist/index.js CHANGED
@@ -5091,6 +5091,202 @@ async function runUpload(options) {
5091
5091
  }
5092
5092
  }
5093
5093
 
5094
+ // src/commands/validate-content.ts
5095
+ import * as fs9 from "fs";
5096
+ import * as path9 from "path";
5097
+ var CJK_REGEX = /[\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff]/g;
5098
+ var WORD_REGEX = /[A-Za-z0-9]+(?:['’\-][A-Za-z0-9]+)*/g;
5099
+ function countMetrics(text) {
5100
+ const chars = [...text].length;
5101
+ const charsNoSpace = [...text.replace(/\s+/g, "")].length;
5102
+ const cjk = (text.match(CJK_REGEX) ?? []).length;
5103
+ const words = (text.match(WORD_REGEX) ?? []).length;
5104
+ const lines = text.length === 0 ? 0 : text.split(/\r?\n/).length;
5105
+ const paragraphs = text.split(/\n\s*\n/).map((p) => p.trim()).filter((p) => p.length > 0).length;
5106
+ return { chars, charsNoSpace, cjk, words, lines, paragraphs };
5107
+ }
5108
+ function pickCount(metrics, countBy) {
5109
+ switch (countBy) {
5110
+ case "chars":
5111
+ return metrics.chars;
5112
+ case "cjk":
5113
+ return metrics.cjk;
5114
+ case "words":
5115
+ return metrics.words;
5116
+ case "chars-no-space":
5117
+ default:
5118
+ return metrics.charsNoSpace;
5119
+ }
5120
+ }
5121
+ function stripMarkdown(input) {
5122
+ let text = input;
5123
+ text = text.replace(/^\uFEFF?---\r?\n[\s\S]*?\r?\n---\r?\n?/, "");
5124
+ text = text.replace(/^[ \t]*(```|~~~).*$/gm, "");
5125
+ text = text.replace(/!\[[^\]]*\]\([^)]*\)/g, "");
5126
+ text = text.replace(/\[([^\]]*)\]\([^)]*\)/g, "$1");
5127
+ text = text.replace(/^[ \t]*#{1,6}[ \t]+/gm, "");
5128
+ text = text.replace(/^[ \t]*>[ \t]?/gm, "");
5129
+ text = text.replace(/^[ \t]*[-*+][ \t]+/gm, "");
5130
+ text = text.replace(/^[ \t]*\d+\.[ \t]+/gm, "");
5131
+ text = text.replace(/^[ \t]*\|?[ \t]*:?-{3,}.*$/gm, "");
5132
+ text = text.replace(/\|/g, " ");
5133
+ text = text.replace(/(\*\*|__|\*|_|`|~~)/g, "");
5134
+ return text;
5135
+ }
5136
+ var LEAK_RULES = [
5137
+ { regex: /(?:TF|PA|MF)-?\d{2,}/g, reason: "\u4E09\u5E93\u5185\u90E8\u7F16\u7801\uFF08TF/PA/MF\uFF09\u4E0D\u5E94\u51FA\u73B0\u5728\u6210\u7A3F" },
5138
+ { regex: /三库溯源/g, reason: "\u4E09\u5E93\u6EAF\u6E90\u5C5E\u4E8E\u8FC7\u7A0B\u8BB0\u5F55\uFF0C\u4E0D\u5E94\u51FA\u73B0\u5728\u6210\u7A3F" },
5139
+ { regex: /流量因子库|产品资产库|烹调方法库/g, reason: "\u4E09\u5E93\u540D\u79F0\u5C5E\u4E8E\u5185\u90E8\u9AA8\u67B6\uFF0C\u4E0D\u5E94\u51FA\u73B0\u5728\u6210\u7A3F" },
5140
+ { regex: /SOP\s*(?:执行|步骤|记录)/gi, reason: "SOP \u8FC7\u7A0B\u5185\u5BB9\u4E0D\u5E94\u51FA\u73B0\u5728\u6210\u7A3F" },
5141
+ { regex: /焊点/g, reason: "\u710A\u70B9\u4E3A\u5185\u90E8\u63D0\u70BC\u672F\u8BED\uFF0C\u4E0D\u5E94\u51FA\u73B0\u5728\u6210\u7A3F" }
5142
+ ];
5143
+ function findLeaks(text, extraForbidden = []) {
5144
+ const hits = [];
5145
+ const seen = /* @__PURE__ */ new Set();
5146
+ for (const rule of LEAK_RULES) {
5147
+ const matches = text.match(rule.regex);
5148
+ if (!matches) continue;
5149
+ for (const m of matches) {
5150
+ const key = `${rule.reason}::${m}`;
5151
+ if (seen.has(key)) continue;
5152
+ seen.add(key);
5153
+ hits.push({ match: m, reason: rule.reason });
5154
+ }
5155
+ }
5156
+ for (const word of extraForbidden) {
5157
+ const w = word.trim();
5158
+ if (!w) continue;
5159
+ if (text.includes(w)) {
5160
+ const key = `\u81EA\u5B9A\u4E49\u7981\u7528\u8BCD::${w}`;
5161
+ if (seen.has(key)) continue;
5162
+ seen.add(key);
5163
+ hits.push({ match: w, reason: "\u547D\u4E2D\u81EA\u5B9A\u4E49\u7981\u7528\u8BCD\uFF08--forbidden\uFF09" });
5164
+ }
5165
+ }
5166
+ return hits;
5167
+ }
5168
+ function parseForbidden(raw) {
5169
+ if (!raw) return [];
5170
+ return raw.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
5171
+ }
5172
+ var COUNT_BY_LABEL = {
5173
+ chars: "\u603B\u5B57\u7B26\u6570\uFF08\u542B\u7A7A\u767D\uFF09",
5174
+ "chars-no-space": "\u5B57\u7B26\u6570\uFF08\u4E0D\u542B\u7A7A\u767D\uFF09",
5175
+ cjk: "\u6C49\u5B57\u6570",
5176
+ words: "\u82F1\u6587\u5355\u8BCD\u6570"
5177
+ };
5178
+ function readStdin() {
5179
+ try {
5180
+ return fs9.readFileSync(0, "utf8");
5181
+ } catch {
5182
+ return "";
5183
+ }
5184
+ }
5185
+ function resolveContent(options) {
5186
+ if (options.text !== void 0) {
5187
+ return { raw: options.text, sourceLabel: "--text \u6587\u672C" };
5188
+ }
5189
+ if (options.file) {
5190
+ const filePath = path9.resolve(options.file);
5191
+ if (!fs9.existsSync(filePath) || !fs9.statSync(filePath).isFile()) {
5192
+ console.error(`
5193
+ \u274C \u6587\u6848\u6587\u4EF6\u4E0D\u5B58\u5728\uFF1A${filePath}`);
5194
+ process.exit(1);
5195
+ }
5196
+ try {
5197
+ return { raw: fs9.readFileSync(filePath, "utf8"), sourceLabel: path9.basename(filePath) };
5198
+ } catch (e) {
5199
+ console.error(`
5200
+ \u274C \u8BFB\u53D6\u6587\u4EF6\u5931\u8D25\uFF1A${e.message}`);
5201
+ process.exit(1);
5202
+ }
5203
+ }
5204
+ if (!process.stdin.isTTY) {
5205
+ const piped = readStdin();
5206
+ if (piped.length > 0) return { raw: piped, sourceLabel: "stdin" };
5207
+ }
5208
+ console.error("\n\u274C \u672A\u63D0\u4F9B\u5F85\u6821\u9A8C\u6587\u6848\u3002\u8BF7\u7528\u4EE5\u4E0B\u4EFB\u4E00\u65B9\u5F0F\u4F20\u5165\uFF08\u5BBF\u4E3B\u65E0\u6587\u4EF6\u5DE5\u5177\u65F6\u7528\u540E\u4E24\u79CD\uFF09\uFF1A");
5209
+ console.error(" 1) \u6587\u4EF6\uFF1A siluzan-cso validate-content -f draft.md --max 800");
5210
+ console.error(' 2) \u7BA1\u9053\uFF1A echo "\u6587\u6848\u5185\u5BB9" | siluzan-cso validate-content --max 800');
5211
+ console.error(' 3) \u884C\u5185\u6587\u672C\uFF1Asiluzan-cso validate-content --text "\u6587\u6848\u5185\u5BB9" --max 800');
5212
+ process.exit(1);
5213
+ }
5214
+ async function runValidateContent(options) {
5215
+ const { raw, sourceLabel } = resolveContent(options);
5216
+ const countBy = options.countBy ?? "chars-no-space";
5217
+ const checkLeak = options.checkLeak !== false;
5218
+ const extraForbidden = parseForbidden(options.forbidden);
5219
+ const textForCount = options.stripMarkdown ? stripMarkdown(raw) : raw;
5220
+ const metrics = countMetrics(textForCount);
5221
+ const value = pickCount(metrics, countBy);
5222
+ const limitProblems = [];
5223
+ if (options.min !== void 0 && value < options.min) {
5224
+ limitProblems.push(
5225
+ `\u5B57\u6570\u4E0D\u8DB3\uFF1A${COUNT_BY_LABEL[countBy]} ${value} < \u4E0B\u9650 ${options.min}\uFF08\u8FD8\u5DEE ${options.min - value}\uFF09`
5226
+ );
5227
+ }
5228
+ if (options.max !== void 0 && value > options.max) {
5229
+ limitProblems.push(
5230
+ `\u5B57\u6570\u8D85\u9650\uFF1A${COUNT_BY_LABEL[countBy]} ${value} > \u4E0A\u9650 ${options.max}\uFF08\u8D85\u51FA ${value - options.max}\uFF09`
5231
+ );
5232
+ }
5233
+ const leaks = checkLeak || extraForbidden.length > 0 ? findLeaks(raw, extraForbidden) : [];
5234
+ const passed = limitProblems.length === 0 && leaks.length === 0;
5235
+ if (options.json) {
5236
+ console.log(
5237
+ JSON.stringify(
5238
+ {
5239
+ source: sourceLabel,
5240
+ countBy,
5241
+ strippedMarkdown: Boolean(options.stripMarkdown),
5242
+ limit: { min: options.min ?? null, max: options.max ?? null, value },
5243
+ metrics,
5244
+ limitProblems,
5245
+ leaks,
5246
+ passed
5247
+ },
5248
+ null,
5249
+ 2
5250
+ )
5251
+ );
5252
+ process.exit(passed ? 0 : 1);
5253
+ }
5254
+ console.log(`
5255
+ \u{1F4C4} \u6587\u6848\u6821\u9A8C\uFF1A${sourceLabel}`);
5256
+ if (options.stripMarkdown) console.log(" \uFF08\u5DF2\u53BB\u9664 markdown \u8BED\u6CD5 / frontmatter \u540E\u7EDF\u8BA1\uFF09");
5257
+ console.log("");
5258
+ console.log(` \u603B\u5B57\u7B26\u6570\uFF08\u542B\u7A7A\u767D\uFF09 ${metrics.chars}`);
5259
+ console.log(` \u5B57\u7B26\u6570\uFF08\u4E0D\u542B\u7A7A\u767D\uFF09 ${metrics.charsNoSpace}`);
5260
+ console.log(` \u6C49\u5B57\u6570 ${metrics.cjk}`);
5261
+ console.log(` \u82F1\u6587\u5355\u8BCD\u6570 ${metrics.words}`);
5262
+ console.log(` \u884C\u6570 / \u6BB5\u843D\u6570 ${metrics.lines} / ${metrics.paragraphs}`);
5263
+ if (options.min !== void 0 || options.max !== void 0) {
5264
+ const range = `${options.min ?? "\u2014"} ~ ${options.max ?? "\u2014"}`;
5265
+ console.log(`
5266
+ \u5B57\u6570\u9650\u5236\uFF08${COUNT_BY_LABEL[countBy]}\uFF09\uFF1A\u5F53\u524D ${value}\uFF0C\u8981\u6C42 ${range}`);
5267
+ }
5268
+ console.log("\n\u6821\u9A8C\u7ED3\u679C\uFF1A");
5269
+ if (limitProblems.length === 0 && (options.min !== void 0 || options.max !== void 0)) {
5270
+ console.log(" \u2713 \u5B57\u6570\u5728\u9650\u5236\u8303\u56F4\u5185");
5271
+ }
5272
+ for (const p of limitProblems) console.log(` \u2717 ${p}`);
5273
+ if (checkLeak || extraForbidden.length > 0) {
5274
+ if (leaks.length === 0) {
5275
+ console.log(" \u2713 \u672A\u53D1\u73B0\u5185\u90E8\u9AA8\u67B6\u8D44\u4EA7 / \u7981\u7528\u8BCD\u6CC4\u6F0F");
5276
+ } else {
5277
+ console.log(` \u2717 \u53D1\u73B0 ${leaks.length} \u5904\u7591\u4F3C\u6CC4\u6F0F\uFF08\u4E0D\u5E94\u51FA\u73B0\u5728\u4EA4\u4ED8\u6210\u7A3F\uFF09\uFF1A`);
5278
+ for (const l of leaks) console.log(` \u300C${l.match}\u300D\u2014 ${l.reason}`);
5279
+ }
5280
+ }
5281
+ if (passed) {
5282
+ console.log("\n\u2705 \u6587\u6848\u6821\u9A8C\u901A\u8FC7");
5283
+ process.exit(0);
5284
+ } else {
5285
+ console.log("\n\u274C \u6587\u6848\u6821\u9A8C\u672A\u901A\u8FC7\uFF0C\u8BF7\u6309\u4E0A\u8FF0\u95EE\u9898\u4FEE\u8BA2\u540E\u91CD\u65B0\u6821\u9A8C");
5286
+ process.exit(1);
5287
+ }
5288
+ }
5289
+
5094
5290
  // src/commands/report/_shared.ts
5095
5291
  var DEFAULT_METHOD = "Day";
5096
5292
  var DEFAULT_WORKS_ORDER = "play";
@@ -5408,8 +5604,8 @@ function printMarkdownFetch(runtime, includeModules, sections) {
5408
5604
  }
5409
5605
 
5410
5606
  // src/commands/report/commands.ts
5411
- import * as fs9 from "fs";
5412
- import * as path9 from "path";
5607
+ import * as fs10 from "fs";
5608
+ import * as path10 from "path";
5413
5609
  async function runReportFetch(options) {
5414
5610
  const runtime = buildRuntime(options);
5415
5611
  const includeModules = parseModules(options.include);
@@ -5593,7 +5789,7 @@ function defaultDownloadPath(recordId) {
5593
5789
  const now = /* @__PURE__ */ new Date();
5594
5790
  const pad = (n) => String(n).padStart(2, "0");
5595
5791
  const name = `operations-report-${recordId}-${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}.pdf`;
5596
- return path9.resolve(process.cwd(), name);
5792
+ return path10.resolve(process.cwd(), name);
5597
5793
  }
5598
5794
  async function downloadFile(url, output) {
5599
5795
  const res = await fetch(url);
@@ -5601,7 +5797,7 @@ async function downloadFile(url, output) {
5601
5797
  throw new Error(`\u4E0B\u8F7D\u5931\u8D25\uFF0CHTTP ${res.status}`);
5602
5798
  }
5603
5799
  const buffer = Buffer.from(await res.arrayBuffer());
5604
- fs9.writeFileSync(output, buffer);
5800
+ fs10.writeFileSync(output, buffer);
5605
5801
  }
5606
5802
  async function runReportDownload(options) {
5607
5803
  if (!options.id) {
@@ -5621,7 +5817,7 @@ async function runReportDownload(options) {
5621
5817
  const msg = error.message;
5622
5818
  exitWithError(`\u83B7\u53D6\u4E0B\u8F7D\u5730\u5740\u5931\u8D25\uFF1A${msg}`);
5623
5819
  }
5624
- const output = path9.resolve(options.output ? options.output : defaultDownloadPath(options.id));
5820
+ const output = path10.resolve(options.output ? options.output : defaultDownloadPath(options.id));
5625
5821
  try {
5626
5822
  await downloadFile(pdfUrl, output);
5627
5823
  } catch (error) {
@@ -5852,8 +6048,8 @@ async function requestPlanning(config, endpoint, init = {}, verbose = false) {
5852
6048
  }
5853
6049
 
5854
6050
  // src/commands/planning/commands.ts
5855
- import * as fs10 from "fs";
5856
- import * as path10 from "path";
6051
+ import * as fs11 from "fs";
6052
+ import * as path11 from "path";
5857
6053
  async function runPlanningEnterprises(options) {
5858
6054
  const config = loadConfig(options.token);
5859
6055
  const verbose = Boolean(options.verbose);
@@ -5981,7 +6177,7 @@ async function watchPlanTask(config, taskId, maxWaitMs, onProgress) {
5981
6177
  const url = `${baseUrl}/api/plans/tasks/${encodeURIComponent(taskId)}/progress`;
5982
6178
  const controller = new AbortController();
5983
6179
  let finished = false;
5984
- return new Promise((resolve6, reject) => {
6180
+ return new Promise((resolve7, reject) => {
5985
6181
  const settle = (fn) => {
5986
6182
  if (finished) return;
5987
6183
  finished = true;
@@ -6028,7 +6224,7 @@ async function watchPlanTask(config, taskId, maxWaitMs, onProgress) {
6028
6224
  }
6029
6225
  onProgress?.(event);
6030
6226
  if (event.status === "completed") {
6031
- settle(() => resolve6(event.plan ?? null));
6227
+ settle(() => resolve7(event.plan ?? null));
6032
6228
  return;
6033
6229
  }
6034
6230
  if (event.status === "failed" || event.status === "cancelled") {
@@ -6359,13 +6555,13 @@ function inferEnterpriseNameFromPlan(plan) {
6359
6555
  }
6360
6556
  async function loadPlanFromSource(options, config) {
6361
6557
  if (options.input) {
6362
- const inputPath = path10.resolve(options.input);
6363
- if (!fs10.existsSync(inputPath)) {
6558
+ const inputPath = path11.resolve(options.input);
6559
+ if (!fs11.existsSync(inputPath)) {
6364
6560
  exitWithError2(`\u8F93\u5165\u6587\u4EF6\u4E0D\u5B58\u5728\uFF1A${inputPath}`);
6365
6561
  }
6366
6562
  let raw = "";
6367
6563
  try {
6368
- raw = fs10.readFileSync(inputPath, "utf-8");
6564
+ raw = fs11.readFileSync(inputPath, "utf-8");
6369
6565
  } catch (error) {
6370
6566
  exitWithError2(`\u8BFB\u53D6\u8F93\u5165\u6587\u4EF6\u5931\u8D25\uFF1A${error.message}`);
6371
6567
  }
@@ -6399,9 +6595,9 @@ async function runPlanningExportTxt(options) {
6399
6595
  const defaultName = sanitizeFilename(
6400
6596
  `\u5185\u5BB9\u9009\u9898\u65B9\u5411\u89C4\u5212_${enterpriseName}_${plan.yearMonth ?? "unknown"}.txt`
6401
6597
  );
6402
- const outputPath = path10.resolve(options.output ? options.output : defaultName);
6598
+ const outputPath = path11.resolve(options.output ? options.output : defaultName);
6403
6599
  try {
6404
- fs10.writeFileSync(outputPath, text, "utf-8");
6600
+ fs11.writeFileSync(outputPath, text, "utf-8");
6405
6601
  } catch (error) {
6406
6602
  exitWithError2(`\u5199\u5165\u5BFC\u51FA\u6587\u4EF6\u5931\u8D25\uFF1A${error.message}`);
6407
6603
  }
@@ -6898,8 +7094,8 @@ async function runAuthorize(opts) {
6898
7094
  }
6899
7095
 
6900
7096
  // src/commands/persona.ts
6901
- import fs11 from "fs";
6902
- import path11 from "path";
7097
+ import fs12 from "fs";
7098
+ import path12 from "path";
6903
7099
  var MAX_PERSONA_NAME = 60;
6904
7100
  function unwrapGetPersonas(raw) {
6905
7101
  if (!raw || typeof raw !== "object") return null;
@@ -7002,15 +7198,15 @@ ${hint}`);
7002
7198
  console.log("\n\u63D0\u793A\uFF1A\u5B8C\u6574 styleGuide\uFF08Markdown\uFF09\u8BF7\u4F7F\u7528 --json \u67E5\u770B\u6BCF\u6761\u8BB0\u5F55\u7684 styleGuide \u5B57\u6BB5\u3002");
7003
7199
  }
7004
7200
  function readStyleGuideFromFile(filePath) {
7005
- const resolved = path11.resolve(process.cwd(), filePath);
7006
- if (!fs11.existsSync(resolved)) {
7201
+ const resolved = path12.resolve(process.cwd(), filePath);
7202
+ if (!fs12.existsSync(resolved)) {
7007
7203
  throw new Error(`styleGuide \u6587\u4EF6\u4E0D\u5B58\u5728\uFF1A${resolved}`);
7008
7204
  }
7009
- const stat = fs11.statSync(resolved);
7205
+ const stat = fs12.statSync(resolved);
7010
7206
  if (!stat.isFile()) {
7011
7207
  throw new Error(`styleGuide \u6587\u4EF6\u8DEF\u5F84\u4E0D\u662F\u666E\u901A\u6587\u4EF6\uFF1A${resolved}`);
7012
7208
  }
7013
- return fs11.readFileSync(resolved, "utf-8");
7209
+ return fs12.readFileSync(resolved, "utf-8");
7014
7210
  }
7015
7211
  function unwrapAddPersona(raw) {
7016
7212
  if (!raw || typeof raw !== "object") return null;
@@ -7545,7 +7741,7 @@ async function runRagList(options) {
7545
7741
  }
7546
7742
 
7547
7743
  // src/commands/config.ts
7548
- import * as fs12 from "fs";
7744
+ import * as fs13 from "fs";
7549
7745
  function cmdConfigShow() {
7550
7746
  const shared = readSharedConfig();
7551
7747
  const envApiKey = process.env.SILUZAN_API_KEY;
@@ -7893,6 +8089,31 @@ function registerCsoCommands(program2) {
7893
8089
  });
7894
8090
  }
7895
8091
  );
8092
+ program2.command("validate-content").description(
8093
+ "\u6821\u9A8C\u6587\u6848\uFF1A\u7EDF\u8BA1\u5B57\u6570\uFF08\u6C49\u5B57/\u5B57\u7B26/\u5355\u8BCD\uFF09\uFF0C\u6309 --min/--max \u68C0\u67E5\u5B57\u6570\u9650\u5236\uFF0C\u5E76\u68C0\u67E5\u4E09\u5E93\u7F16\u7801/\u6EAF\u6E90\u7B49\u5185\u90E8\u5185\u5BB9\u662F\u5426\u6CC4\u6F0F\u5230\u6210\u7A3F\u3002\u6587\u6848\u6765\u6E90\u652F\u6301 -f \u6587\u4EF6\u3001--text \u884C\u5185\u6587\u672C\u3001\u6216 stdin \u7BA1\u9053\uFF08\u5BBF\u4E3B\u65E0\u6587\u4EF6\u5DE5\u5177\u65F6\u7528\u540E\u4E24\u79CD\uFF09"
8094
+ ).option("-f, --file <path>", "\u5F85\u6821\u9A8C\u7684\u6587\u6848\u6587\u4EF6\u8DEF\u5F84\uFF08.md / .txt\uFF09\uFF1B\u4E0E --text / stdin \u4E09\u9009\u4E00").option(
8095
+ "--text <content>",
8096
+ "\u76F4\u63A5\u4F20\u5165\u6587\u6848\u6B63\u6587\uFF08\u5BBF\u4E3B\u65E0\u6587\u4EF6\u5DE5\u5177\u65F6\u7528\uFF09\uFF1B\u4F18\u5148\u7EA7\u9AD8\u4E8E -f \u4E0E stdin"
8097
+ ).option("--min <n>", "\u5B57\u6570\u4E0B\u9650\uFF08\u6309 --count-by \u53E3\u5F84\uFF09", parseInt).option("--max <n>", "\u5B57\u6570\u4E0A\u9650\uFF08\u6309 --count-by \u53E3\u5F84\uFF09", parseInt).option(
8098
+ "--count-by <mode>",
8099
+ "\u9650\u5236\u53E3\u5F84\uFF1Achars-no-space\uFF08\u4E0D\u542B\u7A7A\u767D\uFF0C\u9ED8\u8BA4\uFF09| chars\uFF08\u542B\u7A7A\u767D\uFF09| cjk\uFF08\u6C49\u5B57\uFF09| words\uFF08\u82F1\u6587\u5355\u8BCD\uFF09",
8100
+ "chars-no-space"
8101
+ ).option("--strip-markdown", "\u7EDF\u8BA1\u524D\u53BB\u9664 markdown \u8BED\u6CD5\u4E0E frontmatter\uFF08\u66F4\u8D34\u8FD1\u6B63\u6587\u5B57\u6570\uFF09", false).option("--no-check-leak", "\u8DF3\u8FC7\u5185\u90E8\u9AA8\u67B6\u8D44\u4EA7\uFF08\u4E09\u5E93\u7F16\u7801/\u6EAF\u6E90/SOP \u7B49\uFF09\u6CC4\u6F0F\u68C0\u67E5").option("--forbidden <words>", "\u989D\u5916\u7981\u7528\u5B50\u4E32\uFF0C\u9017\u53F7\u5206\u9694\uFF08\u547D\u4E2D\u5373\u6821\u9A8C\u4E0D\u901A\u8FC7\uFF09").option("--json", "\u4EE5 JSON \u8F93\u51FA\u6821\u9A8C\u7ED3\u679C\uFF08\u9002\u5408\u811A\u672C/\u81EA\u52A8\u5316\uFF09", false).option("--verbose", "\u663E\u793A\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(
8102
+ async (opts) => {
8103
+ await runValidateContent({
8104
+ file: opts.file,
8105
+ text: opts.text,
8106
+ min: opts.min,
8107
+ max: opts.max,
8108
+ countBy: opts.countBy,
8109
+ stripMarkdown: opts.stripMarkdown,
8110
+ checkLeak: opts.checkLeak,
8111
+ forbidden: opts.forbidden,
8112
+ json: opts.json,
8113
+ verbose: opts.verbose
8114
+ });
8115
+ }
8116
+ );
7896
8117
  program2.command("publish").description("\u6309 JSON \u914D\u7F6E\u6587\u4EF6\u63D0\u4EA4\u53D1\u5E03\u4EFB\u52A1\u5230 CSO").requiredOption("-c, --config <path>", "\u53D1\u5E03\u914D\u7F6E\u6587\u4EF6\u8DEF\u5F84\uFF08JSON\uFF09").option("-t, --token <token>", "Token\uFF08\u53EF\u9009\uFF1B\u4F18\u5148\u4E8E ~/.siluzan/config.json\uFF09").option("--dry-run", "\u4EC5\u9884\u89C8\u8BF7\u6C42\u4F53\uFF0C\u4E0D\u5B9E\u9645\u63D0\u4EA4", false).option("--verbose", "\u663E\u793A\u5B8C\u6574\u8BF7\u6C42\u4F53\u53CA\u8BE6\u7EC6\u9519\u8BEF\uFF08\u542B\u654F\u611F\u5B57\u6BB5\uFF0C\u9ED8\u8BA4\u8131\u654F\uFF09", false).action(
7897
8118
  async (opts) => {
7898
8119
  await runPublish({
@@ -29,7 +29,7 @@ Windows 注意:部分 Agent 客户端通过 PowerShell / cmd 代执行命令
29
29
 
30
30
  - **只读**:查询媒体账号列表、账号分组、运营报表、发布任务状态、人设列表、RAG 知识库检索、AI 内容规划详情
31
31
  - **写入**(需用户确认):上传素材、提交发布任务、创建/更新账号分组、生成 AI 内容规划、站内信回复
32
- - **本地文件操作**:`extract-cover` 在本地截取视频帧并输出图片文件;`init` 将 Skill 文件写入 AI 助手目录
32
+ - **本地文件操作**:`extract-cover` 在本地截取视频帧并输出图片文件;`validate-content` 在本地校验文案文件(字数限制 / 内部内容泄漏);`init` 将 Skill 文件写入 AI 助手目录
33
33
 
34
34
  ---
35
35
 
@@ -68,6 +68,7 @@ Windows 注意:部分 Agent 客户端通过 PowerShell / cmd 代执行命令
68
68
  | `siluzan-cso account-group list/create/add-accounts/remove-accounts/update/delete` | 账号分组管理 | `references/account-group.md` |
69
69
  | `siluzan-cso upload -f <file>` | 上传视频 / 图片到素材库 | `references/upload.md` |
70
70
  | `siluzan-cso extract-cover -f <video> -p <平台>` | 从视频截取封面帧 | `references/extract-cover.md` |
71
+ | `siluzan-cso validate-content -f <file>` | 文案落盘后校验:字数统计 / 字数限制(`--min`/`--max`)/ 内部内容泄漏检查 | `references/validate-content.md` |
71
72
  | `siluzan-cso publish -c config.json` | 提交多平台发布任务 | `references/publish.md` |
72
73
  | `siluzan-cso task list/detail/item` | 查看任务状态 / 处理失败 / 重试 | `references/task.md` |
73
74
  | `siluzan-cso report fetch --media <平台>` | 运营报表(核心指标 / 视频排行 / 趋势) | `references/report.md` |
@@ -84,6 +85,7 @@ Windows 注意:部分 Agent 客户端通过 PowerShell / cmd 代执行命令
84
85
  | 发布视频或图文 | `references/publish.md` |
85
86
  | 上传素材 | `references/upload.md` |
86
87
  | 截取视频封面 | `references/extract-cover.md` |
88
+ | 文案写完落盘后校验字数 / 检查内部内容泄漏 | `references/validate-content.md` |
87
89
  | 查发布记录 / 处理失败 | `references/task.md` |
88
90
  | 查账号数据 / 运营报表 | `references/report.md` |
89
91
  | 查找账号 ID 或账号详情 | `references/list-accounts.md` |
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "slug": "siluzan-cso",
3
- "version": "1.1.27-beta.4",
4
- "publishedAt": 1781081180473,
3
+ "version": "1.1.27-beta.5",
4
+ "publishedAt": 1781085763387,
5
5
  "homepage": "https://www.siluzan.com",
6
6
  "source": "https://dev.azure.com/jack4it/Sammamish/_git/siluzan-skill",
7
7
  "requiredBinaries": [
@@ -0,0 +1,114 @@
1
+ # validate-content — 文案校验(字数限制 + 内部内容泄漏)
2
+
3
+ > 对文案做**本地**校验:统计字数、按字数限制检查上下限、并检查三库编码 / 溯源 / SOP 等内部骨架资产是否泄漏到成稿。
4
+ > 纯本地操作,不调用任何接口、不需要鉴权。校验**通过 exit 0,不通过 exit 1**,便于在工作流中卡点。
5
+
6
+ ## 文案来源(三选一)
7
+
8
+ 优先级:`--text` > `-f/--file` > stdin(管道)。**宿主没有文件读写工具时**,用后两种方式直接把对话里的文案传进来,无需落盘:
9
+
10
+ ```bash
11
+ # 1) 文件(宿主有文件工具时最常用)
12
+ siluzan-cso validate-content -f draft.md --max 800
13
+
14
+ # 2) 管道 / heredoc(推荐:长文不受命令行长度限制)
15
+ cat <<'EOF' | siluzan-cso validate-content --max 800
16
+ 这里是要校验的整篇文案……
17
+ 可以包含多行、markdown 等。
18
+ EOF
19
+
20
+ # 3) 行内文本(短文案;过长会受 shell 参数长度限制)
21
+ siluzan-cso validate-content --text "这里是文案内容" --max 280
22
+ ```
23
+
24
+ > 三者都没提供且 stdin 非管道时,命令会报错并打印上述三种用法指引。
25
+
26
+ ---
27
+
28
+ ## 何时使用
29
+
30
+ - 生成口播稿 / 公众号文章 / 博客等成稿并**落盘后**,交付给用户**之前**做一次质检。
31
+ - 用户给出了**字数限制**(如「不超过 800 字」「至少 1500 字」「标题 ≤ 20 字」)时,用 `--min` / `--max` 核对。
32
+ - 担心内部骨架资产(三库内部编码 `TF-/PA-/MF-`、`三库溯源`、`SOP执行`、三库名称、`焊点` 等过程内容)误写进成稿时(默认开启该检查)。
33
+
34
+ ---
35
+
36
+ ## 用法
37
+
38
+ ```bash
39
+ # 最简:只统计字数 + 默认泄漏检查
40
+ siluzan-cso validate-content -f draft.md
41
+
42
+ # 字数上限 800(默认按「不含空白字符数」口径)
43
+ siluzan-cso validate-content -f draft.md --max 800
44
+
45
+ # 字数区间 1500~3000,按汉字数统计
46
+ siluzan-cso validate-content -f article.md --min 1500 --max 3000 --count-by cjk
47
+
48
+ # 去除 markdown 语法后再统计(更贴近用户感知的正文字数)
49
+ siluzan-cso validate-content -f article.md --max 800 --strip-markdown
50
+
51
+ # 关闭内部内容泄漏检查(仅看字数)
52
+ siluzan-cso validate-content -f draft.md --max 280 --no-check-leak
53
+
54
+ # 追加自定义禁用词(命中即不通过)
55
+ siluzan-cso validate-content -f draft.md --forbidden "竞品名,内部代号,占位符"
56
+
57
+ # 脚本/自动化:JSON 输出 + 退出码判定
58
+ siluzan-cso validate-content -f draft.md --max 800 --json
59
+ ```
60
+
61
+ ---
62
+
63
+ ## 参数
64
+
65
+ | 参数 | 说明 |
66
+ | --- | --- |
67
+ | `-f, --file <path>` | 待校验文案文件(`.md` / `.txt`);与 `--text` / stdin 三选一 |
68
+ | `--text <content>` | 直接传入文案正文(宿主无文件工具时用);优先级最高 |
69
+ | `--min <n>` | 字数下限(按 `--count-by` 口径) |
70
+ | `--max <n>` | 字数上限(按 `--count-by` 口径) |
71
+ | `--count-by <mode>` | 限制口径:`chars-no-space`(不含空白,**默认**)· `chars`(含空白)· `cjk`(汉字)· `words`(英文单词) |
72
+ | `--strip-markdown` | 统计前去除 markdown 语法与 frontmatter,默认 `false` |
73
+ | `--no-check-leak` | 跳过内部骨架资产泄漏检查(默认开启检查) |
74
+ | `--forbidden <words>` | 额外禁用子串,逗号分隔,命中即校验不通过 |
75
+ | `--json` | JSON 输出(含全部指标、问题列表、`passed`) |
76
+
77
+ ---
78
+
79
+ ## 字数口径选择建议
80
+
81
+ - **中文成稿(公众号 / 博客 / 口播)**:默认 `chars-no-space` 或 `cjk` 都可;用户说「字数」一般指不含空白的字符数。
82
+ - **标题 / 简介等带英文混排**:`chars-no-space`。
83
+ - **纯英文内容**:用 `words` 统计单词数。
84
+ - **平台硬上限(如 X/Twitter 280 字符)**:用 `chars`(含空白)最贴近平台计数。
85
+
86
+ > markdown 文件里 `#`、`*`、`>` 等符号会被计入原始字符数。需要「正文字数」时加 `--strip-markdown`。
87
+
88
+ ---
89
+
90
+ ## 泄漏检查命中项
91
+
92
+ 默认检查并视为**不应出现在成稿**的内部内容(与 `three-lib-content-workflow` 第 2 步「内容保密」一致):
93
+
94
+ - 三库内部编码:`TF-xxxx` / `PA-xxxx` / `MF-xxxx`
95
+ - `三库溯源`、`SOP执行 / SOP步骤 / SOP记录`、`焊点`
96
+ - 三库名称:`流量因子库` / `产品资产库` / `烹调方法库`
97
+
98
+ 命中后会逐条列出片段与原因;AI 应据此删改成稿后重新校验,**不要**把这些内部内容交付给用户。
99
+
100
+ ---
101
+
102
+ ## 退出码
103
+
104
+ | 退出码 | 含义 |
105
+ | --- | --- |
106
+ | `0` | 校验通过(字数在范围内 + 无泄漏 / 禁用词) |
107
+ | `1` | 文件不存在、字数超/欠限、或命中泄漏 / 禁用词 |
108
+
109
+ ---
110
+
111
+ ## 交叉引用
112
+
113
+ - 成稿创作流程 → `three-lib-content-workflow/content-writer.workflow.md`
114
+ - 成稿落盘与呈现约定 → 同上「输出」一节
@@ -9,7 +9,7 @@ $ErrorActionPreference = 'Stop'
9
9
  # -- Package info (injected at build time) ------------------------------------
10
10
  $PKG_NAME = 'siluzan-cso-cli'
11
11
  # PKG_VERSION 锁定到与本脚本同批构建产物一致的版本,避免与 dist/skill 错位
12
- $PKG_VERSION = '1.1.27-beta.4'
12
+ $PKG_VERSION = '1.1.27-beta.5'
13
13
  $CLI_BIN = 'siluzan-cso'
14
14
  $SKILL_LABEL = 'Siluzan CSO'
15
15
  $INSTALL_CMD = 'npm install -g siluzan-cso-cli@beta'
@@ -9,7 +9,7 @@ set -euo pipefail
9
9
  # -- Package info (injected at build time) ------------------------------------
10
10
  readonly PKG_NAME="siluzan-cso-cli"
11
11
  # PKG_VERSION 锁定到与本脚本同批构建产物一致的版本,避免与 dist/skill 错位
12
- readonly PKG_VERSION="1.1.27-beta.4"
12
+ readonly PKG_VERSION="1.1.27-beta.5"
13
13
  readonly CLI_BIN="siluzan-cso"
14
14
  readonly SKILL_LABEL="Siluzan CSO"
15
15
  readonly INSTALL_CMD="npm install -g siluzan-cso-cli@beta"
@@ -125,16 +125,14 @@
125
125
 
126
126
  ## 输出
127
127
 
128
- - **交付物**:默认只交付成稿(口播脚本 / 文章等);人设卡、选题列表等按任务需要给。**三库拆解 / SOP 摘要 / 溯源记录**不是默认交付物,仅当用户**明确要求**时才给。成稿等长内容**落盘成独立文件**,别堆在对话里。
129
- - **落盘与呈现**(按工具能力降级):
130
- 1. 有「文件呈现 / artifacts」工具(如 `present_files`)→ 落盘到约定路径(如 `/mnt/user-data/outputs/`)后调用呈现。
131
- 2. 否则有「文件写入」工具(如 `write` / `edit_file`)→ 写入工作区目录,并在对话中回报绝对路径。
132
- 3. 都没有 → 直接在对话中给出全文。
128
+ - **交付物**:默认只交付成稿(如文章、口播脚本);人设卡、选题等按需附上。三库拆解、SOP摘要、溯源等内容仅用户明确要求时提供。成稿须落盘为单独文件,不在对话中堆正文。
129
+ - **三库/SOP禁止泄漏**:三库编码、溯源、骨架内容等只作内部创作,严禁写入成稿或输出,除非用户有要求,且仍不得展示内置三库原文或链接。
130
+ - **落盘与呈现**:优先用文件呈现工具(如`present_files`);无则写入目录报告路径,再无则对话中全文展示。
133
131
  - **文件命名**:语义化,如 `<人设>-<选题>-成稿.md`。
134
- - **三库 / SOP 过程内容禁止主动外露**:三库溯源、内部库编码(`TF-xxxx` / `PA-xxxx` / `MF-xxxx`)、SOP 步骤、焊点提炼、自检 / 质检明细均为**内部骨架资产**,只用于驱动创作;**严禁**写进成稿正文或罗列在对话里(如"三库溯源:流量因子 TF-0002……"一律不准出现)。仅当用户**明确要求**时才输出,且仍受第 2 步「内容保密」约束(内置默认三库原文与远端链接永不展示)。
135
- - **过程与成稿分离**:成稿文件**只放最终可直接使用的正文**;过程性内容若需留存,**单独落一个文件**(如 `<人设>-<选题>-SOP执行记录.md`),否则直接丢弃,不在对话中展示。
136
- - **对话只保留极短说明**:摘要、文件名 / 路径、下一步建议;不得粘贴已落盘的长正文,也不得带三库溯源 / 内部编码 / SOP 步骤。
137
- - 文件正文默认 **Markdown**;若后续场景子文档(如口播)对格式另有硬约束(纯文本、禁用装饰符等),**以子文档为准**,扩展名可为 `.md` / `.txt`,「落盘 + 按工具能力呈现」原则不变。
132
+ - **成稿后校验**:每次交付前必须用 `siluzan-cso validate-content` 校验(含字数、三库泄漏等),不通过须修正重新检测。文案来源按宿主能力选:有文件工具用 `-f <成稿文件>`;**无文件工具**(成稿只在对话里)则用管道 `printf '%s' "<全文>" | siluzan-cso validate-content …` `--text "<全文>"` 直接传入。详见 `references/validate-content.md`。
133
+ - **过程与成稿分开**:成稿只含正文,过程内容如需保存单独成文件,不在对话中展示。
134
+ - **对话只简短说明**:仅报摘要、文件名/路径及后续建议,不贴正文、不含三库相关内容。
135
+ - 文件默认为 **Markdown**,特殊需求按场景可用 `.txt` 等,落盘+呈现策略不变。
138
136
 
139
137
  ## 风格规则
140
138
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "siluzan-cso-cli",
3
- "version": "1.1.27-beta.4",
3
+ "version": "1.1.27-beta.5",
4
4
  "description": "Siluzan platform AI Skill CLI — multi-platform content publishing (video/image-text) for Cursor, Claude Code, and OpenClaw.",
5
5
  "keywords": [
6
6
  "ai-skill",