@bty/customer-service-cli 0.4.7 → 0.5.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.
Files changed (4) hide show
  1. package/CHANGELOG.md +286 -0
  2. package/README.md +137 -4
  3. package/dist/bin.js +1250 -46
  4. package/package.json +3 -2
package/dist/bin.js CHANGED
@@ -180,10 +180,10 @@ function readCache() {
180
180
  return null;
181
181
  }
182
182
  }
183
- function writeCache(cache) {
183
+ function writeCache(cache3) {
184
184
  const dir = getConfigDir();
185
185
  fs2.mkdirSync(dir, { recursive: true });
186
- fs2.writeFileSync(getCachePath(), JSON.stringify(cache, null, 2));
186
+ fs2.writeFileSync(getCachePath(), JSON.stringify(cache3, null, 2));
187
187
  }
188
188
  function compareSemver(current, latest) {
189
189
  const parse = (v) => v.replace(/^v/, "").split(".").map(Number);
@@ -235,10 +235,10 @@ async function checkForUpdate(currentVersion) {
235
235
  return null;
236
236
  }
237
237
  try {
238
- const cache = readCache();
238
+ const cache3 = readCache();
239
239
  const now = Date.now();
240
- if (cache && now - cache.lastCheckedAt < CHECK_INTERVAL_MS) {
241
- return compareSemver(currentVersion, cache.latestVersion) < 0 ? formatUpdateMessage(currentVersion, cache.latestVersion) : null;
240
+ if (cache3 && now - cache3.lastCheckedAt < CHECK_INTERVAL_MS) {
241
+ return compareSemver(currentVersion, cache3.latestVersion) < 0 ? formatUpdateMessage(currentVersion, cache3.latestVersion) : null;
242
242
  }
243
243
  const latestVersion = await fetchLatestVersion();
244
244
  writeCache({ latestVersion, lastCheckedAt: now });
@@ -276,7 +276,7 @@ function toExitCode(err) {
276
276
  return 1;
277
277
  }
278
278
  function createRequest(globalTimeout) {
279
- return async function request(baseUrl, path4, options) {
279
+ return async function request(baseUrl, path5, options) {
280
280
  const headers = {
281
281
  "Content-Type": "application/json",
282
282
  ...options.headers
@@ -304,7 +304,7 @@ function createRequest(globalTimeout) {
304
304
  if (workspaceId) {
305
305
  headers["workspace-id"] = workspaceId;
306
306
  }
307
- let url = `${baseUrl}${path4}`;
307
+ let url = `${baseUrl}${path5}`;
308
308
  if (options.query) {
309
309
  const params = new URLSearchParams();
310
310
  for (const [key, value] of Object.entries(options.query)) {
@@ -437,6 +437,7 @@ function isTokenExpired(expiresAt) {
437
437
  var DEFAULT_CS_API_URL = "https://customer-servhub-api.betteryeah.com";
438
438
  var DEFAULT_AI_API_URL = "https://ai-api.betteryeah.com";
439
439
  var DEFAULT_CUSTOMER_AGENT_API_URL = "https://customer-agent.bantouyan.com";
440
+ var DEFAULT_CS_ADMIN_URL = "https://customer-service-admin.betteryeah.com";
440
441
  function getCustomerServiceUrl() {
441
442
  const envUrl = process.env.CS_CS_API_URL;
442
443
  if (envUrl) return envUrl;
@@ -455,6 +456,12 @@ function getCustomerAgentUrl() {
455
456
  const config = readConfig();
456
457
  return config?.customerAgentApiUrl ?? DEFAULT_CUSTOMER_AGENT_API_URL;
457
458
  }
459
+ function getCsAdminUrl() {
460
+ const envUrl = process.env.CS_CS_ADMIN_URL;
461
+ if (envUrl) return envUrl;
462
+ const config = readConfig();
463
+ return config?.csAdminUrl ?? DEFAULT_CS_ADMIN_URL;
464
+ }
458
465
  function getWorkspaceId(overrideWorkspaceId) {
459
466
  const lockState = readEnvLockState();
460
467
  if (lockState.workspaceId) {
@@ -1784,7 +1791,387 @@ async function getIssueStats(opts) {
1784
1791
  return result.data ?? result;
1785
1792
  }
1786
1793
 
1794
+ // src/client/workbench-issues-api.ts
1795
+ async function batchReassignOwner(params) {
1796
+ const request = createRequest();
1797
+ return request(getCsAdminUrl(), "/api/workbench/issues/batch-owner", {
1798
+ method: "POST",
1799
+ body: { issueIds: params.issueIds, ownerUserId: params.ownerUserId }
1800
+ });
1801
+ }
1802
+
1803
+ // src/utils/batch-input.ts
1804
+ import fs5 from "fs";
1805
+ function parseIdsInput(opts) {
1806
+ const hasIds = opts.ids !== void 0 && opts.ids.trim() !== "";
1807
+ const hasFile = opts.idsFile !== void 0 && opts.idsFile.trim() !== "";
1808
+ if (hasIds && hasFile) {
1809
+ throw new Error("--ids \u4E0E --ids-file \u4E92\u65A5\uFF0C\u8BF7\u53EA\u63D0\u4F9B\u5176\u4E00");
1810
+ }
1811
+ if (!hasIds && !hasFile) {
1812
+ throw new Error("\u9700\u8981\u63D0\u4F9B --ids \u6216 --ids-file \u4E4B\u4E00");
1813
+ }
1814
+ let raw;
1815
+ if (hasIds) {
1816
+ raw = opts.ids.split(",").map((s) => s.trim()).filter(Boolean);
1817
+ } else {
1818
+ const filePath = opts.idsFile;
1819
+ let content;
1820
+ try {
1821
+ content = fs5.readFileSync(filePath, "utf8");
1822
+ } catch (err) {
1823
+ const msg = err instanceof Error ? err.message : String(err);
1824
+ throw new Error(`\u8BFB\u53D6 --ids-file \u5931\u8D25: ${msg}`);
1825
+ }
1826
+ raw = content.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
1827
+ }
1828
+ if (raw.length === 0) {
1829
+ throw new Error("--ids / --ids-file \u672A\u89E3\u6790\u5230\u4EFB\u4F55 id");
1830
+ }
1831
+ const seen = /* @__PURE__ */ new Set();
1832
+ const ids = [];
1833
+ for (const id of raw) {
1834
+ if (!seen.has(id)) {
1835
+ seen.add(id);
1836
+ ids.push(id);
1837
+ }
1838
+ }
1839
+ return ids;
1840
+ }
1841
+
1842
+ // src/utils/issue-batch-input.ts
1843
+ import fs6 from "fs";
1844
+ var PRIORITY_VALUES = ["critical", "high", "medium", "low"];
1845
+ function readSource(opts) {
1846
+ const hasFile = typeof opts.file === "string" && opts.file.trim() !== "";
1847
+ const hasStdin = typeof opts.stdin === "string";
1848
+ if (hasFile && hasStdin) {
1849
+ throw new Error("--file \u4E0E --stdin \u4E92\u65A5\uFF0C\u8BF7\u53EA\u63D0\u4F9B\u5176\u4E00");
1850
+ }
1851
+ if (!hasFile && !hasStdin) {
1852
+ throw new Error("\u9700\u8981\u63D0\u4F9B --file <path> \u6216\u901A\u8FC7 stdin \u4F20\u5165 JSON");
1853
+ }
1854
+ if (hasFile) {
1855
+ try {
1856
+ return fs6.readFileSync(opts.file, "utf8");
1857
+ } catch (err) {
1858
+ const msg = err instanceof Error ? err.message : String(err);
1859
+ throw new Error(`\u8BFB\u53D6 --file \u5931\u8D25: ${msg}`);
1860
+ }
1861
+ }
1862
+ return opts.stdin;
1863
+ }
1864
+ function parseIssueBatchInput(opts) {
1865
+ const raw = readSource(opts).trim();
1866
+ if (!raw) {
1867
+ throw new Error("\u8F93\u5165 JSON \u4E3A\u7A7A");
1868
+ }
1869
+ let parsed;
1870
+ try {
1871
+ parsed = JSON.parse(raw);
1872
+ } catch (err) {
1873
+ const msg = err instanceof Error ? err.message : String(err);
1874
+ throw new Error(`JSON \u89E3\u6790\u5931\u8D25: ${msg}`);
1875
+ }
1876
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
1877
+ throw new Error("\u9876\u5C42\u5FC5\u987B\u662F JSON object\uFF08\u542B version + issues[]\uFF09");
1878
+ }
1879
+ const root = parsed;
1880
+ const version2 = typeof root.version === "string" ? root.version : "1";
1881
+ const issuesRaw = root.issues;
1882
+ if (!Array.isArray(issuesRaw)) {
1883
+ throw new Error("\u9876\u5C42\u7F3A\u5C11\u6570\u7EC4\u5B57\u6BB5 issues[]");
1884
+ }
1885
+ if (issuesRaw.length === 0) {
1886
+ throw new Error("issues[] \u4E3A\u7A7A\uFF0C\u65E0\u53EF\u521B\u5EFA");
1887
+ }
1888
+ const workspaceId = typeof root.workspace_id === "string" ? root.workspace_id : void 0;
1889
+ const issues = issuesRaw.map((it, idx) => validateIssue(it, idx));
1890
+ return { version: version2, workspace_id: workspaceId, issues };
1891
+ }
1892
+ function validateIssue(value, idx) {
1893
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1894
+ throw new Error(`issues[${idx}] \u4E0D\u662F object`);
1895
+ }
1896
+ const v = value;
1897
+ const where = `issues[${idx}]`;
1898
+ const title = requireString(v.title, `${where}.title`);
1899
+ const priority = requirePriority(v.priority, `${where}.priority`);
1900
+ const expected = requireString(v.expected, `${where}.expected`);
1901
+ const conversation_id = optionalString(v.conversation_id, `${where}.conversation_id`);
1902
+ const user_name = optionalString(v.user_name, `${where}.user_name`);
1903
+ if (!conversation_id && !user_name) {
1904
+ throw new Error(`${where} \u5FC5\u987B\u63D0\u4F9B conversation_id \u6216 user_name \u4E4B\u4E00`);
1905
+ }
1906
+ const description = optionalString(v.description, `${where}.description`);
1907
+ let tags;
1908
+ if (v.tags !== void 0) {
1909
+ if (!Array.isArray(v.tags) || !v.tags.every((t) => typeof t === "string")) {
1910
+ throw new Error(`${where}.tags \u5FC5\u987B\u662F string[]`);
1911
+ }
1912
+ tags = v.tags;
1913
+ }
1914
+ const source_type = optionalString(v.source_type, `${where}.source_type`);
1915
+ let source_data;
1916
+ if (v.source_data !== void 0) {
1917
+ if (!v.source_data || typeof v.source_data !== "object" || Array.isArray(v.source_data)) {
1918
+ throw new Error(`${where}.source_data \u5FC5\u987B\u662F object`);
1919
+ }
1920
+ source_data = v.source_data;
1921
+ }
1922
+ return {
1923
+ title,
1924
+ priority,
1925
+ expected,
1926
+ description,
1927
+ conversation_id,
1928
+ user_name,
1929
+ tags,
1930
+ source_type,
1931
+ source_data
1932
+ };
1933
+ }
1934
+ function requireString(v, where) {
1935
+ if (typeof v !== "string" || v.trim() === "") {
1936
+ throw new Error(`${where} \u5FC5\u586B\uFF0C\u4E14\u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32`);
1937
+ }
1938
+ return v;
1939
+ }
1940
+ function optionalString(v, where) {
1941
+ if (v === void 0 || v === null) return void 0;
1942
+ if (typeof v !== "string") {
1943
+ throw new Error(`${where} \u5FC5\u987B\u662F\u5B57\u7B26\u4E32`);
1944
+ }
1945
+ return v.trim() === "" ? void 0 : v;
1946
+ }
1947
+ function requirePriority(v, where) {
1948
+ if (typeof v !== "string" || !PRIORITY_VALUES.includes(v)) {
1949
+ throw new Error(
1950
+ `${where} \u5FC5\u987B\u662F ${PRIORITY_VALUES.join("|")} \u4E4B\u4E00\uFF0C\u6536\u5230: ${JSON.stringify(v)}`
1951
+ );
1952
+ }
1953
+ return v;
1954
+ }
1955
+ function buildContent(issue) {
1956
+ const head = issue.description?.trim();
1957
+ if (head) {
1958
+ return `${head}
1959
+
1960
+ ## Expected
1961
+ ${issue.expected}`;
1962
+ }
1963
+ return `## Expected
1964
+ ${issue.expected}`;
1965
+ }
1966
+
1967
+ // src/client/account-roles-api.ts
1968
+ async function listAccountRoles(opts = {}) {
1969
+ const request = createRequest();
1970
+ const query = {};
1971
+ if (opts.role) query.role = opts.role;
1972
+ if (opts.includeInactive) query.includeInactive = "1";
1973
+ const result = await request(getCsAdminUrl(), "/api/account-roles", {
1974
+ method: "GET",
1975
+ query
1976
+ });
1977
+ return result.rows ?? [];
1978
+ }
1979
+
1980
+ // src/utils/resolve-owner.ts
1981
+ var CACHE_TTL_MS = 5 * 60 * 1e3;
1982
+ var cache = null;
1983
+ async function getEngineers() {
1984
+ const now = Date.now();
1985
+ if (cache && cache.expireAt > now) return cache.engineers;
1986
+ const [ka, smb] = await Promise.all([
1987
+ listAccountRoles({ role: "KA_ENGINEER" }),
1988
+ listAccountRoles({ role: "SMB_ENGINEER" })
1989
+ ]);
1990
+ const merged = mergeAndDedupe(ka, smb);
1991
+ cache = { engineers: merged, expireAt: now + CACHE_TTL_MS };
1992
+ return merged;
1993
+ }
1994
+ function mergeAndDedupe(...lists) {
1995
+ const seen = /* @__PURE__ */ new Map();
1996
+ for (const list of lists) {
1997
+ for (const row of list) {
1998
+ const existing = seen.get(row.csAdminUserId);
1999
+ if (!existing) {
2000
+ seen.set(row.csAdminUserId, { ...row });
2001
+ } else {
2002
+ const roles = /* @__PURE__ */ new Set([...existing.roles, ...row.roles]);
2003
+ existing.roles = [...roles];
2004
+ }
2005
+ }
2006
+ }
2007
+ return [...seen.values()].sort((a, b) => a.engineerName.localeCompare(b.engineerName));
2008
+ }
2009
+ async function resolveOwner(input) {
2010
+ const trimmed = input.trim();
2011
+ if (!trimmed) throw new Error("--owner \u4E0D\u80FD\u4E3A\u7A7A");
2012
+ if (/^\d+$/.test(trimmed)) {
2013
+ if (trimmed.length === 11) {
2014
+ throw new Error("--owner \u4E0D\u518D\u63A5\u53D7\u624B\u673A\u53F7\uFF08\u5DF2\u5E9F\u5F03\uFF09\u3002\u8BF7\u4F20 user_id \u6216\u82B1\u540D");
2015
+ }
2016
+ const n = Number(trimmed);
2017
+ if (!Number.isInteger(n) || n <= 0) {
2018
+ throw new Error(`--owner \u6570\u5B57\u89E3\u6790\u5931\u8D25: ${input}`);
2019
+ }
2020
+ return n;
2021
+ }
2022
+ const engineers = await getEngineers();
2023
+ const exact = engineers.filter((e) => e.engineerName === trimmed);
2024
+ if (exact.length === 1) return exact[0].csAdminUserId;
2025
+ if (exact.length > 1) {
2026
+ const list = exact.map((e) => `${e.engineerName}(${e.csAdminUserId})`).join(", ");
2027
+ throw new Error(`--owner "${input}" \u540C\u540D\u591A\u4EBA\uFF1A${list}\u3002\u8BF7\u6539\u4F20 user_id`);
2028
+ }
2029
+ const partial = engineers.filter(
2030
+ (e) => e.engineerName.toLowerCase().includes(trimmed.toLowerCase())
2031
+ );
2032
+ if (partial.length === 1) return partial[0].csAdminUserId;
2033
+ if (partial.length > 1) {
2034
+ const list = partial.map((e) => `${e.engineerName}(${e.csAdminUserId})`).join(", ");
2035
+ throw new Error(`--owner "${input}" \u6A21\u7CCA\u5339\u914D\u591A\u4EBA\uFF1A${list}\u3002\u8BF7\u6539\u7528\u7CBE\u786E\u82B1\u540D\u6216 user_id`);
2036
+ }
2037
+ throw new Error(`--owner "${input}" \u672A\u627E\u5230\u5339\u914D\u7684\u5DE5\u7A0B\u5E08\uFF08\u5171 ${engineers.length} \u4EBA\u5728\u518C\uFF09`);
2038
+ }
2039
+
2040
+ // src/utils/resolve-session.ts
2041
+ var CACHE_TTL_MS2 = 5 * 60 * 1e3;
2042
+ var cache2 = /* @__PURE__ */ new Map();
2043
+ async function resolveSession(opts) {
2044
+ const userName = opts.userName.trim();
2045
+ if (!userName) throw new Error("user_name \u4E0D\u80FD\u4E3A\u7A7A");
2046
+ const cacheKey = `${opts.workspaceId ?? ""}|${userName}`;
2047
+ const now = Date.now();
2048
+ const hit = cache2.get(cacheKey);
2049
+ if (hit && hit.expireAt > now) return hit.conversationId;
2050
+ const resp = await listConversations({
2051
+ userName,
2052
+ workspaceId: opts.workspaceId,
2053
+ pageSize: opts.pageSize ?? 5
2054
+ });
2055
+ const list = (resp.list ?? []).filter((c) => typeof c.conversation_id === "string");
2056
+ if (list.length === 0) {
2057
+ throw new Error(`user_name "${userName}" \u672A\u5339\u914D\u5230\u4EFB\u4F55\u4F1A\u8BDD\uFF08NOT_FOUND\uFF09`);
2058
+ }
2059
+ if (list.length > 1) {
2060
+ const preview = list.slice(0, 3).map((c) => c.conversation_id).join(", ");
2061
+ throw new Error(
2062
+ `user_name "${userName}" \u547D\u4E2D ${list.length} \u6761\u4F1A\u8BDD\uFF08AMBIGUOUS\uFF09\uFF1A${preview}${list.length > 3 ? " ..." : ""}\u3002\u8BF7\u6539\u4F20 conversation_id`
2063
+ );
2064
+ }
2065
+ const conversationId = list[0].conversation_id;
2066
+ cache2.set(cacheKey, { conversationId, expireAt: now + CACHE_TTL_MS2 });
2067
+ return conversationId;
2068
+ }
2069
+
2070
+ // src/utils/run-batch.ts
2071
+ import readline from "readline";
2072
+ var DEFAULT_CONCURRENCY = 5;
2073
+ async function runBatch(opts) {
2074
+ const concurrency = Math.max(1, opts.concurrency ?? DEFAULT_CONCURRENCY);
2075
+ const results = [];
2076
+ let aborted = false;
2077
+ const sigintHandler = () => {
2078
+ if (!aborted) {
2079
+ aborted = true;
2080
+ outputInfo("\u6536\u5230 SIGINT\uFF0C\u7B49\u5F85\u98DE\u884C\u4E2D\u8BF7\u6C42\u7ED3\u675F\u540E\u9000\u51FA...");
2081
+ }
2082
+ };
2083
+ process.on("SIGINT", sigintHandler);
2084
+ try {
2085
+ for (let i = 0; i < opts.ids.length; i += concurrency) {
2086
+ if (aborted) break;
2087
+ const chunk = opts.ids.slice(i, i + concurrency);
2088
+ const settled = await Promise.all(
2089
+ chunk.map(async (id) => {
2090
+ try {
2091
+ return await opts.perItem(id);
2092
+ } catch (err) {
2093
+ return mapErrorToFailure(id, err);
2094
+ }
2095
+ })
2096
+ );
2097
+ results.push(...settled);
2098
+ }
2099
+ } finally {
2100
+ process.off("SIGINT", sigintHandler);
2101
+ }
2102
+ return buildReport(results, aborted);
2103
+ }
2104
+ function mapErrorToFailure(id, err) {
2105
+ if (err instanceof APIError) {
2106
+ return { status: "failed", id, code: `HTTP_${err.statusCode}`, msg: err.message };
2107
+ }
2108
+ const msg = err instanceof Error ? err.message : String(err);
2109
+ return { status: "failed", id, code: "INTERNAL", msg };
2110
+ }
2111
+ function buildReport(results, aborted) {
2112
+ const report = {
2113
+ version: "1",
2114
+ total: results.length,
2115
+ success_count: 0,
2116
+ failed_count: 0,
2117
+ skipped_count: 0,
2118
+ aborted,
2119
+ success_ids: [],
2120
+ failed: [],
2121
+ skipped: []
2122
+ };
2123
+ for (const r of results) {
2124
+ if (r.status === "success") {
2125
+ report.success_count++;
2126
+ report.success_ids.push(r.id);
2127
+ } else if (r.status === "failed") {
2128
+ report.failed_count++;
2129
+ report.failed.push({ id: r.id, code: r.code, msg: r.msg });
2130
+ } else {
2131
+ report.skipped_count++;
2132
+ report.skipped.push({ id: r.id, reason: r.reason });
2133
+ }
2134
+ }
2135
+ return report;
2136
+ }
2137
+ function reportToExitCode(report) {
2138
+ if (report.aborted) return 130;
2139
+ if (report.failed_count === 0) return 0;
2140
+ if (report.failed_count === report.total) return 1;
2141
+ return 2;
2142
+ }
2143
+ var DEFAULT_CONFIRM_THRESHOLD = 20;
2144
+ async function confirmBatch(opts) {
2145
+ const threshold = opts.threshold ?? DEFAULT_CONFIRM_THRESHOLD;
2146
+ if (opts.count < threshold) return;
2147
+ if (opts.yes) return;
2148
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
2149
+ throw new Error(`\u975E TTY \u73AF\u5883\u4E0B\u8981 ${opts.action} ${opts.count} \u6761\uFF0C\u9700\u663E\u5F0F --yes \u624D\u80FD\u6267\u884C`);
2150
+ }
2151
+ const rl = readline.createInterface({
2152
+ input: process.stdin,
2153
+ output: process.stderr
2154
+ });
2155
+ try {
2156
+ const answer = await new Promise((resolve) => {
2157
+ rl.question(`\u5373\u5C06${opts.action} ${opts.count} \u6761\u8BB0\u5F55\u3002\u7EE7\u7EED\uFF1F\u8F93\u5165 yes \u786E\u8BA4: `, resolve);
2158
+ });
2159
+ if (answer.trim().toLowerCase() !== "yes") {
2160
+ throw new Error("\u5DF2\u53D6\u6D88");
2161
+ }
2162
+ } finally {
2163
+ rl.close();
2164
+ }
2165
+ }
2166
+
1787
2167
  // src/commands/issue.ts
2168
+ async function readAllStdin() {
2169
+ const chunks = [];
2170
+ for await (const chunk of process.stdin) {
2171
+ chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
2172
+ }
2173
+ return Buffer.concat(chunks).toString("utf8");
2174
+ }
1788
2175
  function registerIssueCommand(program2) {
1789
2176
  const issue = program2.command("issue").description(
1790
2177
  "\u5DE5\u5355\uFF08Issue\uFF09\u7BA1\u7406 \u2014\u2014 \u8BB0\u5F55 Agent \u56DE\u590D\u5F02\u5E38\u3001\u5BA2\u6237\u6295\u8BC9\u7B49\u5F85\u5904\u7406\u95EE\u9898\u3002\u5DE5\u5355\u662F AI \u4FEE\u590D\u6D41\u7A0B\uFF08issue-repair\uFF09\u7684\u8F93\u5165\u6E90"
@@ -1825,63 +2212,219 @@ function registerIssueCommand(program2) {
1825
2212
  process.exit(toExitCode(err));
1826
2213
  }
1827
2214
  });
1828
- issue.command("create").description("\u624B\u52A8\u521B\u5EFA\u5DE5\u5355").requiredOption("--title <title>", "\u5DE5\u5355\u6807\u9898\uFF08\u7B80\u8981\u63CF\u8FF0\u95EE\u9898\uFF09").requiredOption("--content <content>", "\u5DE5\u5355\u5185\u5BB9\uFF08\u8BE6\u7EC6\u63CF\u8FF0\u95EE\u9898\u73B0\u8C61\u548C\u671F\u671B\u7ED3\u679C\uFF09").option("--priority <priority>", "\u4F18\u5148\u7EA7: critical | high | medium | low").option("--tags <tags...>", "\u6807\u7B7E\uFF08\u591A\u4E2A\u7A7A\u683C\u5206\u9694\uFF0C\u7528\u4E8E\u5206\u7C7B\u7B5B\u9009\uFF09").option("--conversation-id <id>", "\u5173\u8054\u7684\u4F1A\u8BDD ID").option("--record-id <id>", "\u5173\u8054\u7684\u6D88\u606F\u8BB0\u5F55 ID").option("--workspace-id <id>", "\u5DE5\u4F5C\u7A7A\u95F4 ID\uFF08\u9ED8\u8BA4\u4ECE\u73AF\u5883\u53D8\u91CF\u8BFB\u53D6\uFF09").option("--source-type <type>", "\u6765\u6E90\u7C7B\u578B").option("--source-data <json>", "\u6765\u6E90\u6570\u636E\uFF08JSON \u5B57\u7B26\u4E32\uFF09").action(async (opts) => {
2215
+ issue.command("create").description(
2216
+ "\u521B\u5EFA\u5DE5\u5355\u3002\u5355\u6761\uFF1A--title + --content\uFF08+ \u53EF\u9009\u5B57\u6BB5\uFF09\u3002\u6279\u91CF\uFF1A--file <path> \u6216 --stdin \u8BFB JSON\uFF08\u542B workspace_id + issues[]\uFF0C\u6BCF\u6761\u5FC5\u586B title/priority/expected \u548C conversation_id|user_name \u4E8C\u9009\u4E00\uFF09\u3002\u6279\u91CF\u6A21\u5F0F N>20 \u4E14 TTY \u9700\u8981\u4E8C\u6B21\u786E\u8BA4\uFF1B\u975E TTY \u5FC5\u987B\u663E\u5F0F --yes"
2217
+ ).option("--title <title>", "[\u5355\u6761] \u5DE5\u5355\u6807\u9898\uFF08\u7B80\u8981\u63CF\u8FF0\u95EE\u9898\uFF09").option("--content <content>", "[\u5355\u6761] \u5DE5\u5355\u5185\u5BB9\uFF08\u8BE6\u7EC6\u63CF\u8FF0\u95EE\u9898\u73B0\u8C61\u548C\u671F\u671B\u7ED3\u679C\uFF09").option("--priority <priority>", "[\u5355\u6761] \u4F18\u5148\u7EA7: critical | high | medium | low").option("--tags <tags...>", "[\u5355\u6761] \u6807\u7B7E\uFF08\u591A\u4E2A\u7A7A\u683C\u5206\u9694\uFF0C\u7528\u4E8E\u5206\u7C7B\u7B5B\u9009\uFF09").option("--conversation-id <id>", "[\u5355\u6761] \u5173\u8054\u7684\u4F1A\u8BDD ID").option("--record-id <id>", "[\u5355\u6761] \u5173\u8054\u7684\u6D88\u606F\u8BB0\u5F55 ID").option("--workspace-id <id>", "[\u5355\u6761] \u5DE5\u4F5C\u7A7A\u95F4 ID\uFF08\u9ED8\u8BA4\u4ECE\u73AF\u5883\u53D8\u91CF\u8BFB\u53D6\uFF09").option("--source-type <type>", "[\u5355\u6761] \u6765\u6E90\u7C7B\u578B").option("--source-data <json>", "[\u5355\u6761] \u6765\u6E90\u6570\u636E\uFF08JSON \u5B57\u7B26\u4E32\uFF09").option("--file <path>", "[\u6279\u91CF] \u4ECE JSON \u6587\u4EF6\u8BFB\u8F93\u5165\uFF08\u4E0E --stdin \u4E92\u65A5\uFF09").option("--stdin", "[\u6279\u91CF] \u4ECE stdin \u8BFB JSON\uFF08\u4E0E --file \u4E92\u65A5\uFF09", false).option("--dry-run", "[\u6279\u91CF] \u9884\u89C8\uFF1A\u5B8C\u6210 user_name \u53CD\u67E5 + content \u62FC\u88C5\uFF0C\u4F46\u4E0D\u8C03\u521B\u5EFA\u63A5\u53E3", false).option("--concurrency <n>", "[\u6279\u91CF] \u5E76\u53D1\u6570\uFF08\u9ED8\u8BA4 5\uFF09", "5").option("--yes", "[\u6279\u91CF] \u8DF3\u8FC7 N>=20 \u4E8C\u6B21\u786E\u8BA4\uFF08\u975E TTY \u5FC5\u987B\u663E\u5F0F\u4F20\uFF09", false).action(async (opts) => {
2218
+ const hasBatch = Boolean(opts.file) || opts.stdin === true;
2219
+ const hasSingleLike = Boolean(opts.title || opts.content);
2220
+ if (hasBatch && hasSingleLike) {
2221
+ reportCaughtError(
2222
+ new Error("\u4E0D\u80FD\u540C\u65F6\u63D0\u4F9B\u5355\u6761\u5B57\u6BB5\uFF08--title/--content\uFF09\u548C\u6279\u91CF\u5B57\u6BB5\uFF08--file/--stdin\uFF09")
2223
+ );
2224
+ process.exit(1);
2225
+ return;
2226
+ }
2227
+ if (!hasBatch) {
2228
+ try {
2229
+ if (!opts.title || !opts.content) {
2230
+ throw new Error(
2231
+ "\u5355\u6761\u521B\u5EFA\u9700\u63D0\u4F9B --title \u548C --content\uFF08\u6216\u6539\u7528 --file/--stdin \u6279\u91CF\u6A21\u5F0F\uFF09"
2232
+ );
2233
+ }
2234
+ const data = await createIssue({
2235
+ title: opts.title,
2236
+ content: opts.content,
2237
+ priority: opts.priority,
2238
+ tags: opts.tags,
2239
+ conversation_id: opts.conversationId,
2240
+ record_id: opts.recordId,
2241
+ workspace_id: opts.workspaceId,
2242
+ source_type: opts.sourceType,
2243
+ source_data: opts.sourceData ? JSON.parse(opts.sourceData) : void 0
2244
+ });
2245
+ formatOutput({ success: true, data }, program2.opts().table);
2246
+ } catch (err) {
2247
+ reportCaughtError(err);
2248
+ process.exit(toExitCode(err));
2249
+ }
2250
+ return;
2251
+ }
2252
+ let batchReport = null;
1829
2253
  try {
1830
- const data = await createIssue({
1831
- title: opts.title,
1832
- content: opts.content,
1833
- priority: opts.priority,
1834
- tags: opts.tags,
1835
- conversation_id: opts.conversationId,
1836
- record_id: opts.recordId,
1837
- workspace_id: opts.workspaceId,
1838
- source_type: opts.sourceType,
1839
- source_data: opts.sourceData ? JSON.parse(opts.sourceData) : void 0
2254
+ const stdinContent = opts.stdin ? await readAllStdin() : void 0;
2255
+ const parsed = parseIssueBatchInput({ file: opts.file, stdin: stdinContent });
2256
+ const workspaceId = parsed.workspace_id;
2257
+ const resolved = await resolveSessionsForIssues(parsed.issues, workspaceId);
2258
+ if (opts.dryRun) {
2259
+ formatOutput(
2260
+ {
2261
+ success: true,
2262
+ data: {
2263
+ dry_run: true,
2264
+ version: parsed.version,
2265
+ workspace_id: workspaceId,
2266
+ total: parsed.issues.length,
2267
+ will_create: resolved.map((r) => ({
2268
+ title: r.issue.title,
2269
+ priority: r.issue.priority,
2270
+ conversation_id: r.conversationId,
2271
+ tags: r.issue.tags,
2272
+ resolved_from_user_name: r.issue.conversation_id ? void 0 : r.issue.user_name,
2273
+ preview_content: buildContent(r.issue)
2274
+ }))
2275
+ }
2276
+ },
2277
+ program2.opts().table
2278
+ );
2279
+ return;
2280
+ }
2281
+ await confirmBatch({ count: parsed.issues.length, action: "\u521B\u5EFA", yes: opts.yes });
2282
+ const concurrency = Number(opts.concurrency);
2283
+ if (!Number.isFinite(concurrency) || concurrency < 1) {
2284
+ throw new Error(`--concurrency \u5FC5\u987B\u4E3A\u6B63\u6574\u6570\uFF0C\u6536\u5230: ${opts.concurrency}`);
2285
+ }
2286
+ const ids = parsed.issues.map((_, idx) => `idx-${idx}`);
2287
+ const indexed = new Map(resolved.map((r, idx) => [`idx-${idx}`, r]));
2288
+ batchReport = await runBatch({
2289
+ ids,
2290
+ concurrency,
2291
+ perItem: async (id) => {
2292
+ const r = indexed.get(id);
2293
+ if (!r) throw new Error(`internal: missing resolved entry for ${id}`);
2294
+ const data = await createIssue({
2295
+ title: r.issue.title,
2296
+ content: buildContent(r.issue),
2297
+ priority: r.issue.priority,
2298
+ tags: r.issue.tags,
2299
+ conversation_id: r.conversationId,
2300
+ workspace_id: workspaceId,
2301
+ source_type: r.issue.source_type,
2302
+ source_data: r.issue.source_data
2303
+ });
2304
+ return { status: "success", id, data };
2305
+ }
1840
2306
  });
1841
- formatOutput({ success: true, data }, program2.opts().table);
2307
+ formatOutput({ success: true, data: batchReport }, program2.opts().table);
1842
2308
  } catch (err) {
1843
2309
  reportCaughtError(err);
1844
2310
  process.exit(toExitCode(err));
2311
+ return;
2312
+ }
2313
+ if (batchReport !== null) {
2314
+ const code = reportToExitCode(batchReport);
2315
+ if (code !== 0) process.exit(code);
1845
2316
  }
1846
2317
  });
1847
- issue.command("update").description("\u66F4\u65B0\u5DE5\u5355\u5B57\u6BB5\uFF08\u5982 status\u3001priority\u3001tags\u3001title\u3001content\uFF09").argument("<issue_id>", "\u5DE5\u5355 ID\uFF08\u4ECE issue list \u83B7\u53D6\uFF09").requiredOption(
2318
+ issue.command("update").description(
2319
+ "\u66F4\u65B0\u5DE5\u5355\u5B57\u6BB5\uFF08\u5982 status\u3001priority\u3001tags\u3001title\u3001content\uFF09\u3002\u5355\u6761\uFF1A\u4F20 <issue_id>\u3002\u6279\u91CF\uFF1A\u4F20 --ids \u6216 --ids-file\uFF0C\u6240\u6709 ID \u5E94\u7528\u540C\u4E00 --data\u3002\u6279\u91CF\u6A21\u5F0F\u4E0B N>20 \u4E14 TTY \u9700\u8981\u4E8C\u6B21\u786E\u8BA4\uFF1B\u975E TTY \u5FC5\u987B\u663E\u5F0F --yes"
2320
+ ).argument("[issue_id]", "\u5DE5\u5355 ID\uFF08\u5355\u6761\u66F4\u65B0\u65F6\u5FC5\u586B\uFF1B\u6279\u91CF\u66F4\u65B0\u8D70 --ids / --ids-file\uFF09").requiredOption(
1848
2321
  "--data <json>",
1849
2322
  "JSON \u6570\u636E\u6216 @\u6587\u4EF6\u8DEF\u5F84\u3002\u53EF\u66F4\u65B0: status\uFF08open|closed\uFF09\u3001priority\u3001tags\u3001title\u3001content"
1850
- ).action(async (issueId, opts) => {
2323
+ ).option("--ids <ids>", "\u6279\u91CF\u66F4\u65B0\uFF1A\u9017\u53F7\u5206\u9694\u7684\u5DE5\u5355 ID\uFF08\u4E0E <issue_id> \u4E92\u65A5\uFF09").option("--ids-file <path>", "\u6279\u91CF\u66F4\u65B0\uFF1A\u4ECE\u6587\u4EF6\u8BFB\u53D6 ID\uFF08\u6BCF\u884C\u4E00\u4E2A\uFF1B# \u5F00\u5934\u6CE8\u91CA\uFF09").option("--dry-run", "\u6279\u91CF\u6A21\u5F0F\u9884\u89C8\uFF1A\u53EA\u6253\u5370 will-update \u5217\u8868\uFF0C\u4E0D\u8C03\u5199\u63A5\u53E3", false).option("--concurrency <n>", "\u6279\u91CF\u5E76\u53D1\u6570\uFF08\u9ED8\u8BA4 5\uFF09", "5").option("--yes", "\u6279\u91CF\u6A21\u5F0F\u8DF3\u8FC7\u4E8C\u6B21\u786E\u8BA4\uFF08\u975E TTY \u5FC5\u987B\u663E\u5F0F\u4F20\uFF09", false).action(async (issueId, opts) => {
2324
+ const hasSingle = issueId !== void 0;
2325
+ const hasBatch = opts.ids !== void 0 || opts.idsFile !== void 0;
2326
+ if (hasSingle && hasBatch) {
2327
+ reportCaughtError(new Error("\u4E0D\u80FD\u540C\u65F6\u63D0\u4F9B <issue_id> \u548C --ids/--ids-file"));
2328
+ process.exit(1);
2329
+ return;
2330
+ }
2331
+ if (!hasSingle && !hasBatch) {
2332
+ reportCaughtError(new Error("\u9700\u8981\u63D0\u4F9B <issue_id> \u6216 --ids/--ids-file \u4E4B\u4E00"));
2333
+ process.exit(1);
2334
+ return;
2335
+ }
2336
+ let batchReport = null;
1851
2337
  try {
1852
2338
  const body = parseDataOption(opts.data);
1853
- const data = await updateIssue(issueId, body);
1854
- formatOutput({ success: true, data }, program2.opts().table);
2339
+ if (hasSingle) {
2340
+ const data = await updateIssue(issueId, body);
2341
+ formatOutput({ success: true, data }, program2.opts().table);
2342
+ return;
2343
+ }
2344
+ const ids = parseIdsInput({ ids: opts.ids, idsFile: opts.idsFile });
2345
+ if (opts.dryRun) {
2346
+ formatOutput(
2347
+ {
2348
+ success: true,
2349
+ data: {
2350
+ dry_run: true,
2351
+ total: ids.length,
2352
+ ids,
2353
+ will_apply: body
2354
+ }
2355
+ },
2356
+ program2.opts().table
2357
+ );
2358
+ return;
2359
+ }
2360
+ await confirmBatch({ count: ids.length, action: "\u66F4\u65B0", yes: opts.yes });
2361
+ const concurrency = Number(opts.concurrency);
2362
+ if (!Number.isFinite(concurrency) || concurrency < 1) {
2363
+ throw new Error(`--concurrency \u5FC5\u987B\u4E3A\u6B63\u6574\u6570\uFF0C\u6536\u5230: ${opts.concurrency}`);
2364
+ }
2365
+ batchReport = await runBatch({
2366
+ ids,
2367
+ concurrency,
2368
+ perItem: async (id) => {
2369
+ const data = await updateIssue(id, body);
2370
+ return { status: "success", id, data };
2371
+ }
2372
+ });
2373
+ formatOutput({ success: true, data: batchReport }, program2.opts().table);
1855
2374
  } catch (err) {
1856
2375
  reportCaughtError(err);
1857
2376
  process.exit(toExitCode(err));
2377
+ return;
2378
+ }
2379
+ if (batchReport !== null) {
2380
+ const code = reportToExitCode(batchReport);
2381
+ if (code !== 0) process.exit(code);
1858
2382
  }
1859
2383
  });
1860
2384
  issue.command("update-owner").description(
1861
- "\u66F4\u65B0\u5DE5\u5355\u8D1F\u8D23\u4EBA\uFF08owner\uFF09\u3002\u4EC5\u4E8C\u9009\u4E00\uFF1A--owner <user_id> \u6216 --owner-phone <\u624B\u673A\u53F7>\uFF0C\u540E\u8005\u7531\u670D\u52A1\u7AEF\u89E3\u6790\u4E3A user_id\u3002\u6CE8\u610F\uFF1A\u540E\u7AEF\u9700\u5F00\u542F\u5BF9 owner / owner_phone \u5B57\u6BB5\u7684\u652F\u6301\u540E\u8BE5\u547D\u4EE4\u624D\u80FD\u751F\u6548"
1862
- ).argument("<issue_id>", "\u5DE5\u5355 ID\uFF08\u4ECE issue list \u83B7\u53D6\uFF09").option("--owner <user_id>", "\u8D1F\u8D23\u4EBA user_id\uFF08\u6B63\u6574\u6570\uFF09").option("--owner-phone <phone>", "\u8D1F\u8D23\u4EBA\u624B\u673A\u53F7\uFF08\u7531\u670D\u52A1\u7AEF\u89E3\u6790\u4E3A user_id\uFF09").action(async (issueId, opts) => {
2385
+ "\u66F4\u65B0\u5DE5\u5355\u8D1F\u8D23\u4EBA\u3002--owner \u63A5\u53D7 user_id\uFF08\u22647 \u4F4D\u7EAF\u6570\u5B57\uFF09\u6216\u82B1\u540D\uFF08\u81EA\u52A8\u901A\u8FC7 cs-admin /api/account-roles \u89E3\u6790\uFF09\u3002\u5355\u6761\uFF1A\u4F20 <issue_id>\u3002\u6279\u91CF\uFF1A\u4F20 --ids \u6216 --ids-file\uFF0C\u8D70 cs-admin BFF \u7684 batch-owner\uFF08\u5355\u6B21\u8C03\u7528\u3001\u539F\u5B50\u6539\u6D3E\u3001\u81EA\u52A8 Feishu/DingTalk \u901A\u77E5\uFF09"
2386
+ ).argument("[issue_id]", "\u5DE5\u5355 ID\uFF08\u5355\u6761\u66F4\u65B0\u65F6\u5FC5\u586B\uFF1B\u6279\u91CF\u8D70 --ids / --ids-file\uFF09").requiredOption("--owner <user_id_or_name>", "\u8D1F\u8D23\u4EBA user_id \u6216\u82B1\u540D").option("--ids <ids>", "\u6279\u91CF\u6539\u6D3E\uFF1A\u9017\u53F7\u5206\u9694\u7684\u5DE5\u5355 ID\uFF08\u4E0E <issue_id> \u4E92\u65A5\uFF09").option("--ids-file <path>", "\u6279\u91CF\u6539\u6D3E\uFF1A\u4ECE\u6587\u4EF6\u8BFB\u53D6 ID\uFF08\u6BCF\u884C\u4E00\u4E2A\uFF1B# \u5F00\u5934\u6CE8\u91CA\uFF09").option("--dry-run", "\u6279\u91CF\u6A21\u5F0F\u9884\u89C8\uFF1A\u4EC5\u89E3\u6790 owner + \u5217\u51FA ID\uFF0C\u4E0D\u8C03\u5199\u63A5\u53E3", false).option("--yes", "\u6279\u91CF\u6A21\u5F0F\u8DF3\u8FC7 N>=20 \u4E8C\u6B21\u786E\u8BA4\uFF08\u975E TTY \u5FC5\u987B\u663E\u5F0F\u4F20\uFF09", false).action(async (issueId, opts) => {
2387
+ const hasSingle = issueId !== void 0;
2388
+ const hasBatch = opts.ids !== void 0 || opts.idsFile !== void 0;
2389
+ if (hasSingle && hasBatch) {
2390
+ reportCaughtError(new Error("\u4E0D\u80FD\u540C\u65F6\u63D0\u4F9B <issue_id> \u548C --ids/--ids-file"));
2391
+ process.exit(1);
2392
+ return;
2393
+ }
2394
+ if (!hasSingle && !hasBatch) {
2395
+ reportCaughtError(new Error("\u9700\u8981\u63D0\u4F9B <issue_id> \u6216 --ids/--ids-file \u4E4B\u4E00"));
2396
+ process.exit(1);
2397
+ return;
2398
+ }
1863
2399
  try {
1864
- const hasOwner = opts.owner !== void 0;
1865
- const hasPhone = opts.ownerPhone !== void 0;
1866
- if (hasOwner === hasPhone) {
1867
- throw new Error("\u5FC5\u987B\u4E14\u4EC5\u80FD\u63D0\u4F9B --owner \u6216 --owner-phone \u5176\u4E2D\u4E00\u4E2A");
2400
+ const ownerId = await resolveOwner(String(opts.owner));
2401
+ if (hasSingle) {
2402
+ const data = await updateIssueOwner(issueId, { owner: ownerId });
2403
+ formatOutput({ success: true, data }, program2.opts().table);
2404
+ return;
1868
2405
  }
1869
- const body = {};
1870
- if (hasOwner) {
1871
- const ownerId = Number(opts.owner);
1872
- if (!Number.isInteger(ownerId) || ownerId <= 0) {
1873
- throw new Error(`--owner \u5FC5\u987B\u4E3A\u6B63\u6574\u6570\uFF0C\u6536\u5230: ${opts.owner}`);
1874
- }
1875
- body.owner = ownerId;
1876
- } else {
1877
- const phone = String(opts.ownerPhone).trim();
1878
- if (!phone) {
1879
- throw new Error("--owner-phone \u4E0D\u80FD\u4E3A\u7A7A");
1880
- }
1881
- body.owner_phone = phone;
2406
+ const ids = parseIdsInput({ ids: opts.ids, idsFile: opts.idsFile });
2407
+ if (ids.length > 200) {
2408
+ throw new Error(`\u6279\u91CF\u6539\u6D3E\u5355\u6279\u6700\u591A 200 \u6761\uFF0C\u5F53\u524D ${ids.length}\u3002\u8BF7\u62C6\u5206\u8F93\u5165`);
1882
2409
  }
1883
- const data = await updateIssueOwner(issueId, body);
1884
- formatOutput({ success: true, data }, program2.opts().table);
2410
+ if (opts.dryRun) {
2411
+ formatOutput(
2412
+ {
2413
+ success: true,
2414
+ data: {
2415
+ dry_run: true,
2416
+ total: ids.length,
2417
+ ids,
2418
+ resolved_owner_id: ownerId
2419
+ }
2420
+ },
2421
+ program2.opts().table
2422
+ );
2423
+ return;
2424
+ }
2425
+ await confirmBatch({ count: ids.length, action: "\u6539\u6D3E", yes: opts.yes });
2426
+ const result = await batchReassignOwner({ issueIds: ids, ownerUserId: ownerId });
2427
+ formatOutput({ success: true, data: result }, program2.opts().table);
1885
2428
  } catch (err) {
1886
2429
  reportCaughtError(err);
1887
2430
  process.exit(toExitCode(err));
@@ -1919,9 +2462,30 @@ function registerIssueCommand(program2) {
1919
2462
  }
1920
2463
  });
1921
2464
  }
2465
+ async function resolveSessionsForIssues(issues, workspaceId) {
2466
+ const out = [];
2467
+ for (let i = 0; i < issues.length; i++) {
2468
+ const it = issues[i];
2469
+ let conversationId;
2470
+ if (it.conversation_id) {
2471
+ conversationId = it.conversation_id;
2472
+ } else if (it.user_name) {
2473
+ try {
2474
+ conversationId = await resolveSession({ userName: it.user_name, workspaceId });
2475
+ } catch (err) {
2476
+ const msg = err instanceof Error ? err.message : String(err);
2477
+ throw new Error(`issues[${i}] \u53CD\u67E5\u4F1A\u8BDD\u5931\u8D25\uFF1A${msg}`);
2478
+ }
2479
+ } else {
2480
+ throw new Error(`issues[${i}] \u65E2\u65E0 conversation_id \u4E5F\u65E0 user_name`);
2481
+ }
2482
+ out.push({ issue: it, conversationId });
2483
+ }
2484
+ return out;
2485
+ }
1922
2486
 
1923
2487
  // src/commands/knowledge.ts
1924
- import fs5 from "fs";
2488
+ import fs7 from "fs";
1925
2489
 
1926
2490
  // src/client/knowledge-api.ts
1927
2491
  async function listExternalKnowledge(opts) {
@@ -2027,7 +2591,7 @@ async function deleteKnowledgeContent(opts) {
2027
2591
  // src/commands/knowledge.ts
2028
2592
  function readContentArg(value) {
2029
2593
  if (value.startsWith("@")) {
2030
- return fs5.readFileSync(value.slice(1), "utf-8");
2594
+ return fs7.readFileSync(value.slice(1), "utf-8");
2031
2595
  }
2032
2596
  return value;
2033
2597
  }
@@ -3447,6 +4011,645 @@ function registerSACommand(program2) {
3447
4011
  });
3448
4012
  }
3449
4013
 
4014
+ // src/commands/testset.ts
4015
+ import fs9 from "fs";
4016
+
4017
+ // src/utils/file-output.ts
4018
+ import fs8 from "fs";
4019
+ import path4 from "path";
4020
+ async function writeBinaryToFile(filePath, buffer) {
4021
+ try {
4022
+ const dir = path4.dirname(filePath);
4023
+ fs8.mkdirSync(dir, { recursive: true });
4024
+ fs8.writeFileSync(filePath, buffer);
4025
+ } catch (err) {
4026
+ const msg = err instanceof Error ? err.message : String(err);
4027
+ throw new APIError(1, `\u6587\u4EF6\u5199\u5165\u5931\u8D25: ${msg}`);
4028
+ }
4029
+ return { path: filePath, bytes: buffer.length };
4030
+ }
4031
+
4032
+ // src/client/testset-api.ts
4033
+ function unwrapPaginated(raw, fallbackPageSize) {
4034
+ return {
4035
+ items: Array.isArray(raw?.data) ? raw?.data : [],
4036
+ total: typeof raw?.total === "number" ? raw.total : 0,
4037
+ page: typeof raw?.page_no === "number" ? raw.page_no : 1,
4038
+ pageSize: typeof raw?.page_size === "number" ? raw.page_size : fallbackPageSize
4039
+ };
4040
+ }
4041
+ async function listTestSets(query) {
4042
+ const request = createRequest();
4043
+ const raw = await request(
4044
+ getCustomerServiceUrl(),
4045
+ "/v1/test_sets",
4046
+ {
4047
+ method: "GET",
4048
+ query: {
4049
+ customer_agent_config_id: query.customerAgentConfigId,
4050
+ keyword: query.keyword,
4051
+ page: query.page,
4052
+ page_size: query.pageSize
4053
+ }
4054
+ }
4055
+ );
4056
+ return unwrapPaginated(raw, query.pageSize ?? 20);
4057
+ }
4058
+ async function getTestSet(id) {
4059
+ const request = createRequest();
4060
+ return request(getCustomerServiceUrl(), `/v1/test_sets/${id}`, {
4061
+ method: "GET"
4062
+ });
4063
+ }
4064
+ async function updateTestSet(id, body) {
4065
+ const request = createRequest();
4066
+ return request(getCustomerServiceUrl(), `/v1/test_sets/${id}`, {
4067
+ method: "PUT",
4068
+ body
4069
+ });
4070
+ }
4071
+ async function deleteTestSet(id) {
4072
+ const request = createRequest();
4073
+ return request(getCustomerServiceUrl(), `/v1/test_sets/${id}`, {
4074
+ method: "DELETE"
4075
+ });
4076
+ }
4077
+ async function duplicateTestSet(id, body) {
4078
+ const request = createRequest();
4079
+ return request(getCustomerServiceUrl(), `/v1/test_sets/${id}/duplicate`, {
4080
+ method: "POST",
4081
+ body
4082
+ });
4083
+ }
4084
+ async function getEvalPrompt(id) {
4085
+ const request = createRequest();
4086
+ return request(getCustomerServiceUrl(), `/v1/test_sets/${id}/evaluate-prompt`, {
4087
+ method: "GET"
4088
+ });
4089
+ }
4090
+ async function setEvalPrompt(id, body) {
4091
+ const request = createRequest();
4092
+ return request(getCustomerServiceUrl(), `/v1/test_sets/${id}/evaluate-prompt`, {
4093
+ method: "PUT",
4094
+ body
4095
+ });
4096
+ }
4097
+ async function listCases(testSetId, query = {}) {
4098
+ const request = createRequest();
4099
+ const raw = await request(
4100
+ getCustomerServiceUrl(),
4101
+ `/v1/test_sets/${testSetId}/cases`,
4102
+ {
4103
+ method: "GET",
4104
+ query: {
4105
+ page: query.page,
4106
+ page_size: query.pageSize
4107
+ }
4108
+ }
4109
+ );
4110
+ return unwrapPaginated(raw, query.pageSize ?? 20);
4111
+ }
4112
+ async function createCase(testSetId, body) {
4113
+ const request = createRequest();
4114
+ return request(getCustomerServiceUrl(), `/v1/test_sets/${testSetId}/cases`, {
4115
+ method: "POST",
4116
+ body
4117
+ });
4118
+ }
4119
+ async function updateCase(testSetId, caseId, body) {
4120
+ const request = createRequest();
4121
+ return request(getCustomerServiceUrl(), `/v1/test_sets/${testSetId}/cases/${caseId}`, {
4122
+ method: "PUT",
4123
+ body
4124
+ });
4125
+ }
4126
+ async function deleteCase(testSetId, caseId) {
4127
+ const request = createRequest();
4128
+ return request(getCustomerServiceUrl(), `/v1/test_sets/${testSetId}/cases/${caseId}`, {
4129
+ method: "DELETE"
4130
+ });
4131
+ }
4132
+ async function listBatches(query) {
4133
+ const request = createRequest();
4134
+ const raw = await request(
4135
+ getCustomerServiceUrl(),
4136
+ "/v1/test_sets/execution_batches",
4137
+ {
4138
+ method: "GET",
4139
+ query: {
4140
+ test_set_id: query.testSetId,
4141
+ customer_agent_config_id: query.customerAgentConfigId,
4142
+ status: query.status,
4143
+ keyword: query.keyword,
4144
+ page: query.page,
4145
+ page_size: query.pageSize
4146
+ }
4147
+ }
4148
+ );
4149
+ return unwrapPaginated(raw, query.pageSize ?? 20);
4150
+ }
4151
+ async function getBatch(batchId) {
4152
+ const request = createRequest();
4153
+ return request(getCustomerServiceUrl(), `/v1/test_sets/execution_batches/${batchId}`, {
4154
+ method: "GET"
4155
+ });
4156
+ }
4157
+ async function getBatchStatus(batchId) {
4158
+ const request = createRequest();
4159
+ return request(
4160
+ getCustomerServiceUrl(),
4161
+ `/v1/test_sets/execution_batches/${batchId}/status`,
4162
+ {
4163
+ method: "GET"
4164
+ }
4165
+ );
4166
+ }
4167
+ async function executeBatch(testSetId, customerAgentConfigId) {
4168
+ const request = createRequest();
4169
+ return request(getCustomerServiceUrl(), "/v1/test_sets/execute", {
4170
+ method: "POST",
4171
+ body: {
4172
+ test_set_id: testSetId,
4173
+ customer_agent_config_id: customerAgentConfigId
4174
+ }
4175
+ });
4176
+ }
4177
+ async function rerunCase(batchId, caseId) {
4178
+ const request = createRequest();
4179
+ return request(
4180
+ getCustomerServiceUrl(),
4181
+ `/v1/test_sets/execution_batches/${batchId}/cases/${caseId}/rerun`,
4182
+ {
4183
+ method: "POST"
4184
+ }
4185
+ );
4186
+ }
4187
+ function buildAuthAndWorkspaceHeaders() {
4188
+ const creds = readCredentials();
4189
+ if (!creds) {
4190
+ throw new APIError(2, "\u672A\u767B\u5F55\uFF0C\u8BF7\u8FD0\u884C: cs-cli auth login");
4191
+ }
4192
+ if (isTokenExpired(creds.expiresAt)) {
4193
+ clearCredentials();
4194
+ throw new APIError(2, "Token \u5DF2\u8FC7\u671F\uFF0C\u8BF7\u8FD0\u884C: cs-cli auth login");
4195
+ }
4196
+ const envLock = readEnvLockState();
4197
+ const config = readConfig();
4198
+ let workspaceId;
4199
+ if (envLock.workspaceId) {
4200
+ assertNoWorkspaceOverride(getRuntimeWorkspaceId());
4201
+ workspaceId = envLock.workspaceId;
4202
+ } else {
4203
+ workspaceId = getRuntimeWorkspaceId() ?? config?.defaultWorkspaceId;
4204
+ }
4205
+ if (!workspaceId) {
4206
+ throw new APIError(1, "\u672A\u8BBE\u7F6E\u5DE5\u4F5C\u7A7A\u95F4\uFF0C\u8BF7\u8FD0\u884C: cs-cli config set-workspace <id>");
4207
+ }
4208
+ return {
4209
+ Authorization: `Bearer ${creds.accessToken}`,
4210
+ "workspace-id": workspaceId
4211
+ };
4212
+ }
4213
+ async function exportBatchToFile(testSetId, batchId, outputPath) {
4214
+ const headers = buildAuthAndWorkspaceHeaders();
4215
+ const baseUrl = getCustomerServiceUrl();
4216
+ const timeoutMs = getRuntimeRequestTimeoutMs() ?? 6e4;
4217
+ const url = `${baseUrl}/v1/test_sets/${testSetId}/export?batch_id=${encodeURIComponent(batchId)}`;
4218
+ const response = await fetch(url, {
4219
+ method: "GET",
4220
+ headers,
4221
+ signal: AbortSignal.timeout(timeoutMs)
4222
+ });
4223
+ if (!response.ok) {
4224
+ if (response.status === 401) clearCredentials();
4225
+ throw new APIError(response.status, `\u5BFC\u51FA\u5931\u8D25 HTTP ${response.status}`);
4226
+ }
4227
+ const contentType = response.headers.get("content-type") ?? "";
4228
+ if (!contentType.toLowerCase().includes("spreadsheetml")) {
4229
+ throw new APIError(1, `\u5BFC\u51FA\u54CD\u5E94 Content-Type \u975E xlsx: ${contentType || "<\u7A7A>"}`);
4230
+ }
4231
+ const arrayBuf = await response.arrayBuffer();
4232
+ const buf = Buffer.from(arrayBuf);
4233
+ await writeBinaryToFile(outputPath, buf);
4234
+ return { path: outputPath, bytes: buf.byteLength };
4235
+ }
4236
+
4237
+ // src/commands/batch.ts
4238
+ function registerBatchCommand(rootProgram, parent = rootProgram) {
4239
+ const batch = parent.command("batch").description(
4240
+ "\u6267\u884C\u6279\u6B21\uFF08ExecutionBatch\uFF09\u7BA1\u7406 \u2014\u2014 \u6D4B\u8BD5\u96C6\u56DE\u5F52\u6267\u884C\u540E\u7684\u6279\u6B21\u5217\u8868 / \u8BE6\u60C5 / \u72B6\u6001\u67E5\u8BE2\u3002"
4241
+ );
4242
+ batch.command("list").description("\u5217\u51FA\u6267\u884C\u6279\u6B21\uFF08--testset \u53EF\u9009\uFF1B\u7F3A\u7701\u5217\u5F53\u524D workspace \u5168\u90E8\u6279\u6B21\uFF09").option("--testset <id>", "\u6309\u6D4B\u8BD5\u96C6\u7B5B\u9009").option("--customer-agent-config-id <id>", "\u6309 Agent \u914D\u7F6E\u7B5B\u9009").option("--status <status>", "\u6309\u72B6\u6001\u7B5B\u9009\uFF08\u5982 running / finished / failed\uFF09").option("--keyword <text>", "\u6309\u5173\u952E\u8BCD\u6A21\u7CCA\u641C\u7D22").option("--page <number>", "\u9875\u7801", "1").option("--page-size <number>", "\u6BCF\u9875\u6570\u91CF", "20").action(async (opts) => {
4243
+ try {
4244
+ const result = await listBatches({
4245
+ testSetId: opts.testset,
4246
+ customerAgentConfigId: opts.customerAgentConfigId,
4247
+ status: opts.status,
4248
+ keyword: opts.keyword,
4249
+ page: Number(opts.page),
4250
+ pageSize: Number(opts.pageSize)
4251
+ });
4252
+ formatOutput(
4253
+ {
4254
+ success: true,
4255
+ data: result.items,
4256
+ pagination: {
4257
+ page: result.page,
4258
+ pageSize: result.pageSize,
4259
+ total: result.total
4260
+ }
4261
+ },
4262
+ rootProgram.opts().table
4263
+ );
4264
+ } catch (err) {
4265
+ reportCaughtError(err);
4266
+ process.exit(toExitCode(err));
4267
+ }
4268
+ });
4269
+ batch.command("show").description("\u67E5\u770B\u6279\u6B21\u8BE6\u60C5\uFF08summary\uFF09").argument("<batch_id>", "\u6279\u6B21 ID").action(async (batchId) => {
4270
+ try {
4271
+ const data = await getBatch(batchId);
4272
+ formatOutput({ success: true, data }, rootProgram.opts().table);
4273
+ } catch (err) {
4274
+ reportCaughtError(err);
4275
+ process.exit(toExitCode(err));
4276
+ }
4277
+ });
4278
+ batch.command("status").description("\u67E5\u770B\u6279\u6B21\u5B9E\u65F6\u72B6\u6001\uFF08\u542B is_terminal \u5B57\u6BB5\uFF09").argument("<batch_id>", "\u6279\u6B21 ID").action(async (batchId) => {
4279
+ try {
4280
+ const data = await getBatchStatus(batchId);
4281
+ formatOutput({ success: true, data }, rootProgram.opts().table);
4282
+ } catch (err) {
4283
+ reportCaughtError(err);
4284
+ process.exit(toExitCode(err));
4285
+ }
4286
+ });
4287
+ }
4288
+
4289
+ // src/commands/case.ts
4290
+ var SHOW_PAGE_SIZE = 200;
4291
+ var SHOW_MAX_PAGES = 1e3;
4292
+ function registerCaseCommand(rootProgram, parent = rootProgram) {
4293
+ const caseCmd = parent.command("case").description(
4294
+ "\u6D4B\u8BD5\u7528\u4F8B\uFF08TestCase\uFF09\u7BA1\u7406 \u2014\u2014 \u5355\u6761 case \u7684 CRUD\uFF0C\u6240\u6709\u5B50\u547D\u4EE4\u9700\u8981 --testset \u951A\u5B9A\u5F52\u5C5E\u6D4B\u8BD5\u96C6\u3002"
4295
+ );
4296
+ caseCmd.command("list").description("\u5217\u51FA\u6307\u5B9A\u6D4B\u8BD5\u96C6\u4E0B\u7684\u7528\u4F8B").requiredOption("--testset <id>", "\u6D4B\u8BD5\u96C6 ID\uFF08\u5FC5\u586B\uFF09").option("--page <number>", "\u9875\u7801", "1").option("--page-size <number>", "\u6BCF\u9875\u6570\u91CF", "20").action(async (opts) => {
4297
+ try {
4298
+ const result = await listCases(opts.testset, {
4299
+ page: Number(opts.page),
4300
+ pageSize: Number(opts.pageSize)
4301
+ });
4302
+ formatOutput(
4303
+ {
4304
+ success: true,
4305
+ data: result.items,
4306
+ pagination: {
4307
+ page: result.page,
4308
+ pageSize: result.pageSize,
4309
+ total: result.total
4310
+ }
4311
+ },
4312
+ rootProgram.opts().table
4313
+ );
4314
+ } catch (err) {
4315
+ reportCaughtError(err);
4316
+ process.exit(toExitCode(err));
4317
+ }
4318
+ });
4319
+ caseCmd.command("show").description("\u67E5\u770B\u5355\u6761\u7528\u4F8B\u8BE6\u60C5\uFF08\u5185\u90E8\u7528 list+filter \u5B9E\u73B0\uFF0C\u81EA\u52A8\u7FFB\u9875\uFF09").argument("<case_id>", "\u7528\u4F8B ID").requiredOption("--testset <id>", "\u6D4B\u8BD5\u96C6 ID\uFF08\u5FC5\u586B\uFF09").action(async (caseId, opts) => {
4320
+ try {
4321
+ let page = 1;
4322
+ let accumulated = 0;
4323
+ while (page <= SHOW_MAX_PAGES) {
4324
+ const result = await listCases(opts.testset, {
4325
+ page,
4326
+ pageSize: SHOW_PAGE_SIZE
4327
+ });
4328
+ const items = result.items ?? [];
4329
+ const hit = items.find((it) => it?.case_id === caseId);
4330
+ if (hit) {
4331
+ formatOutput({ success: true, data: hit }, rootProgram.opts().table);
4332
+ return;
4333
+ }
4334
+ accumulated += items.length;
4335
+ const isLastPage = items.length === 0 || accumulated >= result.total;
4336
+ if (isLastPage) break;
4337
+ page += 1;
4338
+ }
4339
+ outputError(1, `\u7528\u4F8B ${caseId} \u4E0D\u5B58\u5728`);
4340
+ process.exit(1);
4341
+ } catch (err) {
4342
+ reportCaughtError(err);
4343
+ process.exit(toExitCode(err));
4344
+ }
4345
+ });
4346
+ caseCmd.command("create").description("\u65B0\u5EFA\u7528\u4F8B").requiredOption("--testset <id>", "\u6D4B\u8BD5\u96C6 ID\uFF08\u5FC5\u586B\uFF09").requiredOption("--data <json|@file>", "\u7528\u4F8B JSON body \u6216 @file \u8DEF\u5F84\uFF08\u5FC5\u586B\uFF09").action(async (opts) => {
4347
+ try {
4348
+ const body = parseDataOption(opts.data);
4349
+ const data = await createCase(opts.testset, body);
4350
+ formatOutput({ success: true, data }, rootProgram.opts().table);
4351
+ } catch (err) {
4352
+ reportCaughtError(err);
4353
+ process.exit(toExitCode(err));
4354
+ }
4355
+ });
4356
+ caseCmd.command("update").description("\u66F4\u65B0\u7528\u4F8B").argument("<case_id>", "\u7528\u4F8B ID").requiredOption("--testset <id>", "\u6D4B\u8BD5\u96C6 ID\uFF08\u5FC5\u586B\uFF09").requiredOption("--data <json|@file>", "\u66F4\u65B0 JSON body \u6216 @file \u8DEF\u5F84\uFF08\u5FC5\u586B\uFF09").action(async (caseId, opts) => {
4357
+ try {
4358
+ const body = parseDataOption(opts.data);
4359
+ const data = await updateCase(opts.testset, caseId, body);
4360
+ formatOutput({ success: true, data }, rootProgram.opts().table);
4361
+ } catch (err) {
4362
+ reportCaughtError(err);
4363
+ process.exit(toExitCode(err));
4364
+ }
4365
+ });
4366
+ caseCmd.command("delete").description("\u5220\u9664\u7528\u4F8B\uFF08\u4E0D\u5F39\u4E8C\u6B21\u786E\u8BA4\uFF09").argument("<case_id>", "\u7528\u4F8B ID").requiredOption("--testset <id>", "\u6D4B\u8BD5\u96C6 ID\uFF08\u5FC5\u586B\uFF09").action(async (caseId, opts) => {
4367
+ try {
4368
+ const data = await deleteCase(opts.testset, caseId);
4369
+ formatOutput({ success: true, data }, rootProgram.opts().table);
4370
+ } catch (err) {
4371
+ reportCaughtError(err);
4372
+ process.exit(toExitCode(err));
4373
+ }
4374
+ });
4375
+ }
4376
+
4377
+ // src/commands/export.ts
4378
+ function registerExportCommand(rootProgram, parent = rootProgram) {
4379
+ parent.command("export").description(
4380
+ "\u5BFC\u51FA\u6279\u6B21\u7ED3\u679C\u4E3A xlsx \u6587\u4EF6\uFF08GET /test_sets/{id}/export?batch_id=<id>\uFF0C\u843D\u76D8\u5230 --output\uFF09"
4381
+ ).requiredOption("--testset <id>", "\u6D4B\u8BD5\u96C6 ID\uFF08\u5FC5\u586B\uFF09").requiredOption("--batch <id>", "\u6279\u6B21 ID\uFF08\u5FC5\u586B\uFF09").requiredOption("--output <path>", "\u843D\u76D8\u6587\u4EF6\u8DEF\u5F84\uFF08\u5FC5\u586B\uFF0C\u7236\u76EE\u5F55\u81EA\u52A8 mkdir -p\uFF09").action(async (opts) => {
4382
+ try {
4383
+ const result = await exportBatchToFile(opts.testset, opts.batch, opts.output);
4384
+ formatOutput({ success: true, data: result }, rootProgram.opts().table);
4385
+ } catch (err) {
4386
+ reportCaughtError(err);
4387
+ process.exit(toExitCode(err));
4388
+ }
4389
+ });
4390
+ }
4391
+
4392
+ // src/commands/run-case.ts
4393
+ function registerRunCaseCommand(rootProgram, parent = rootProgram) {
4394
+ parent.command("run-case").description(
4395
+ "\u91CD\u8DD1\u6307\u5B9A\u6279\u6B21\u5185\u7684\u5355\u6761\u7528\u4F8B\uFF08POST /test_sets/execution_batches/{B}/cases/{C}/rerun\uFF09"
4396
+ ).requiredOption("--batch <id>", "\u6279\u6B21 ID\uFF08\u5FC5\u586B\uFF09").requiredOption("--case <id>", "\u7528\u4F8B ID\uFF08\u5FC5\u586B\uFF09").action(async (opts) => {
4397
+ try {
4398
+ const data = await rerunCase(opts.batch, opts.case);
4399
+ formatOutput({ success: true, data }, rootProgram.opts().table);
4400
+ } catch (err) {
4401
+ reportCaughtError(err);
4402
+ process.exit(toExitCode(err));
4403
+ }
4404
+ });
4405
+ }
4406
+
4407
+ // src/utils/poll.ts
4408
+ var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
4409
+ async function pollUntilDone(opts) {
4410
+ const {
4411
+ intervalMs,
4412
+ timeoutSec,
4413
+ fetch: fetchFn,
4414
+ isDone,
4415
+ maxConsecutiveFailures = 3,
4416
+ now = Date.now,
4417
+ sleep = defaultSleep
4418
+ } = opts;
4419
+ const start = now();
4420
+ let lastResult;
4421
+ let lastError;
4422
+ let consecutiveFailures = 0;
4423
+ while (true) {
4424
+ const elapsedMs = now() - start;
4425
+ if (elapsedMs >= timeoutSec * 1e3) {
4426
+ return {
4427
+ done: false,
4428
+ timedOut: true,
4429
+ lastResult,
4430
+ elapsedSec: Math.floor(elapsedMs / 1e3)
4431
+ };
4432
+ }
4433
+ try {
4434
+ const result = await fetchFn();
4435
+ lastResult = result;
4436
+ consecutiveFailures = 0;
4437
+ if (isDone(result)) {
4438
+ return { done: true, result };
4439
+ }
4440
+ } catch (err) {
4441
+ lastError = err;
4442
+ consecutiveFailures += 1;
4443
+ if (consecutiveFailures >= maxConsecutiveFailures) {
4444
+ return { done: false, failed: true, lastError };
4445
+ }
4446
+ }
4447
+ const afterFetchElapsedMs = now() - start;
4448
+ if (afterFetchElapsedMs >= timeoutSec * 1e3) {
4449
+ return {
4450
+ done: false,
4451
+ timedOut: true,
4452
+ lastResult,
4453
+ elapsedSec: Math.floor(afterFetchElapsedMs / 1e3)
4454
+ };
4455
+ }
4456
+ await sleep(intervalMs);
4457
+ }
4458
+ }
4459
+
4460
+ // src/commands/run.ts
4461
+ var DEFAULT_POLL_INTERVAL_MS = 3e3;
4462
+ var DEFAULT_TIMEOUT_SEC = 600;
4463
+ function extractBatchId(resp) {
4464
+ if (resp && typeof resp === "object" && "batch_id" in resp) {
4465
+ const v = resp.batch_id;
4466
+ if (typeof v === "string") return v;
4467
+ }
4468
+ return "";
4469
+ }
4470
+ function registerRunCommand(rootProgram, parent = rootProgram) {
4471
+ parent.command("run").description("\u89E6\u53D1\u6D4B\u8BD5\u96C6\u56DE\u5F52\u6267\u884C\uFF08\u9ED8\u8BA4\u5F02\u6B65\uFF1B--wait \u5207\u540C\u6B65\u8F6E\u8BE2\uFF0C\u8D85\u65F6\u4ECD exit 0 / ADR-002\uFF09").requiredOption("--testset <id>", "\u6D4B\u8BD5\u96C6 ID\uFF08\u5FC5\u586B\uFF09").requiredOption("--agent <id>", "Agent \u914D\u7F6E ID\uFF08\u5FC5\u586B\uFF0C\u5BF9\u5E94\u540E\u7AEF customer_agent_config_id\uFF09").option("--wait", "\u540C\u6B65\u7B49\u5F85\u7EC8\u6001\uFF08poll /status\uFF0C3s \u95F4\u9694\uFF0C--timeout \u63A7\u5236\u4E0A\u9650\uFF0C\u5355\u4F4D\uFF1A\u79D2\uFF09", false).option(
4472
+ "--timeout <sec>",
4473
+ "\u540C\u6B65\u7B49\u5F85\u4E0A\u9650\uFF08\u5355\u4F4D\uFF1A\u79D2\uFF1B\u4E0E\u5168\u5C40 --request-timeout \u7684\u6BEB\u79D2\u4E0D\u540C\uFF09",
4474
+ String(DEFAULT_TIMEOUT_SEC)
4475
+ ).action(async (opts) => {
4476
+ let exitCode = 0;
4477
+ try {
4478
+ const triggerResp = await executeBatch(opts.testset, opts.agent);
4479
+ const batchId = extractBatchId(triggerResp);
4480
+ if (!opts.wait) {
4481
+ formatOutput({ success: true, data: { batch_id: batchId } }, rootProgram.opts().table);
4482
+ } else {
4483
+ const timeoutSec = Number(opts.timeout) || DEFAULT_TIMEOUT_SEC;
4484
+ const startedAt = Date.now();
4485
+ const pollResult = await pollUntilDone({
4486
+ intervalMs: DEFAULT_POLL_INTERVAL_MS,
4487
+ timeoutSec,
4488
+ fetch: () => getBatchStatus(batchId),
4489
+ isDone: (r) => r?.is_terminal === true
4490
+ });
4491
+ if (pollResult.done) {
4492
+ const elapsedSec = Math.floor((Date.now() - startedAt) / 1e3);
4493
+ formatOutput(
4494
+ {
4495
+ success: true,
4496
+ data: {
4497
+ ...pollResult.result,
4498
+ elapsed_sec: elapsedSec
4499
+ }
4500
+ },
4501
+ rootProgram.opts().table
4502
+ );
4503
+ } else if ("timedOut" in pollResult && pollResult.timedOut) {
4504
+ const last = pollResult.lastResult ?? {};
4505
+ formatOutput(
4506
+ {
4507
+ success: true,
4508
+ data: {
4509
+ batch_id: batchId,
4510
+ status: last.status,
4511
+ is_terminal: false,
4512
+ timeout: true,
4513
+ elapsed_sec: pollResult.elapsedSec,
4514
+ total_count: last.total_count,
4515
+ pass_count: last.pass_count,
4516
+ fail_count: last.fail_count
4517
+ }
4518
+ },
4519
+ rootProgram.opts().table
4520
+ );
4521
+ } else if ("failed" in pollResult && pollResult.failed) {
4522
+ reportCaughtError(pollResult.lastError);
4523
+ const mapped = toExitCode(pollResult.lastError);
4524
+ exitCode = mapped === 1 ? 3 : mapped;
4525
+ }
4526
+ }
4527
+ } catch (err) {
4528
+ reportCaughtError(err);
4529
+ exitCode = toExitCode(err);
4530
+ }
4531
+ process.exit(exitCode);
4532
+ });
4533
+ }
4534
+
4535
+ // src/commands/testset.ts
4536
+ function readPromptInput(value) {
4537
+ if (value.startsWith("@")) {
4538
+ const filePath = value.slice(1);
4539
+ if (!filePath) {
4540
+ throw new Error("File path cannot be empty after @");
4541
+ }
4542
+ return fs9.readFileSync(filePath, "utf-8");
4543
+ }
4544
+ return value;
4545
+ }
4546
+ function registerTestsetCommand(program2) {
4547
+ const testset = program2.command("testset").description(
4548
+ "\u6D4B\u8BD5\u96C6\uFF08TestSet\uFF09\u7BA1\u7406 \u2014\u2014 \u5BF9\u8BDD\u56DE\u5F52\u6D4B\u8BD5\u7528\u4F8B\u96C6\u5408\uFF0C\u914D\u5408 batch / run / export \u5F62\u6210\u95ED\u73AF\u3002"
4549
+ );
4550
+ testset.command("list").description("\u5217\u51FA\u6D4B\u8BD5\u96C6").requiredOption("--customer-agent-config-id <id>", "Agent \u914D\u7F6E ID\uFF08\u5FC5\u586B\uFF0C\u4E0E\u540E\u7AEF\u5951\u7EA6\u5BF9\u9F50\uFF09").option("--keyword <text>", "\u6309\u540D\u79F0\u5173\u952E\u8BCD\u6A21\u7CCA\u641C\u7D22").option("--page <number>", "\u9875\u7801", "1").option("--page-size <number>", "\u6BCF\u9875\u6570\u91CF", "20").action(async (opts) => {
4551
+ try {
4552
+ const result = await listTestSets({
4553
+ customerAgentConfigId: opts.customerAgentConfigId,
4554
+ keyword: opts.keyword,
4555
+ page: Number(opts.page),
4556
+ pageSize: Number(opts.pageSize)
4557
+ });
4558
+ formatOutput(
4559
+ {
4560
+ success: true,
4561
+ data: result.items,
4562
+ pagination: {
4563
+ page: result.page,
4564
+ pageSize: result.pageSize,
4565
+ total: result.total
4566
+ }
4567
+ },
4568
+ program2.opts().table
4569
+ );
4570
+ } catch (err) {
4571
+ reportCaughtError(err);
4572
+ process.exit(toExitCode(err));
4573
+ }
4574
+ });
4575
+ testset.command("show").description("\u67E5\u770B\u5355\u4E2A\u6D4B\u8BD5\u96C6\u8BE6\u60C5").argument("<id>", "\u6D4B\u8BD5\u96C6 ID").action(async (id) => {
4576
+ try {
4577
+ const data = await getTestSet(id);
4578
+ formatOutput({ success: true, data }, program2.opts().table);
4579
+ } catch (err) {
4580
+ reportCaughtError(err);
4581
+ process.exit(toExitCode(err));
4582
+ }
4583
+ });
4584
+ testset.command("update").description("\u66F4\u65B0\u6D4B\u8BD5\u96C6\uFF08\u81F3\u5C11\u63D0\u4F9B --name / --description / --data \u4E4B\u4E00\uFF1B--data \u4F18\u5148\u5408\u5E76\uFF09").argument("<id>", "\u6D4B\u8BD5\u96C6 ID").option("--name <text>", "\u6D4B\u8BD5\u96C6\u540D\u79F0").option("--description <text>", "\u63CF\u8FF0").option("--data <json|@file>", "\u5B8C\u6574 JSON body\uFF08\u8986\u76D6 --name / --description\uFF09").action(async (id, opts) => {
4585
+ try {
4586
+ let body = {};
4587
+ if (opts.name !== void 0) body.test_set_name = opts.name;
4588
+ if (opts.description !== void 0) body.description = opts.description;
4589
+ if (opts.data !== void 0) {
4590
+ const parsed = parseDataOption(opts.data);
4591
+ body = { ...body, ...parsed };
4592
+ }
4593
+ if (Object.keys(body).length === 0) {
4594
+ outputError(1, "\u81F3\u5C11\u63D0\u4F9B --name/--description/--data \u4E4B\u4E00");
4595
+ process.exit(1);
4596
+ }
4597
+ const data = await updateTestSet(id, body);
4598
+ formatOutput({ success: true, data }, program2.opts().table);
4599
+ } catch (err) {
4600
+ reportCaughtError(err);
4601
+ process.exit(toExitCode(err));
4602
+ }
4603
+ });
4604
+ testset.command("delete").description("\u5220\u9664\u6D4B\u8BD5\u96C6\uFF08\u76F4\u63A5\u6267\u884C\uFF0C\u4E0D\u5F39\u4E8C\u6B21\u786E\u8BA4\uFF09").argument("<id>", "\u6D4B\u8BD5\u96C6 ID").action(async (id) => {
4605
+ try {
4606
+ const data = await deleteTestSet(id);
4607
+ formatOutput({ success: true, data }, program2.opts().table);
4608
+ } catch (err) {
4609
+ reportCaughtError(err);
4610
+ process.exit(toExitCode(err));
4611
+ }
4612
+ });
4613
+ testset.command("copy").description("\u590D\u5236\u6D4B\u8BD5\u96C6").argument("<id>", "\u6E90\u6D4B\u8BD5\u96C6 ID").option("--name <text>", "\u526F\u672C\u540D\u79F0").action(async (id, opts) => {
4614
+ try {
4615
+ const data = await duplicateTestSet(id, { test_set_name: opts.name });
4616
+ formatOutput({ success: true, data }, program2.opts().table);
4617
+ } catch (err) {
4618
+ reportCaughtError(err);
4619
+ process.exit(toExitCode(err));
4620
+ }
4621
+ });
4622
+ testset.command("get-eval-prompt").description("\u67E5\u770B\u6D4B\u8BD5\u96C6\u7684\u8BC4\u4EF7\u63D0\u793A\u8BCD").argument("<id>", "\u6D4B\u8BD5\u96C6 ID").option("--raw", "\u88F8\u6587\u672C\u8F93\u51FA\u5230 stdout\uFF08\u4E0D\u5E26 JSON \u5305\u88C5\uFF09", false).action(async (id, opts) => {
4623
+ try {
4624
+ const data = await getEvalPrompt(id);
4625
+ if (opts.raw) {
4626
+ const text = data?.prompt_template ?? "";
4627
+ process.stdout.write(text);
4628
+ return;
4629
+ }
4630
+ formatOutput({ success: true, data }, program2.opts().table);
4631
+ } catch (err) {
4632
+ reportCaughtError(err);
4633
+ process.exit(toExitCode(err));
4634
+ }
4635
+ });
4636
+ testset.command("set-eval-prompt").description("\u8BBE\u7F6E\u6D4B\u8BD5\u96C6\u7684\u8BC4\u4EF7\u63D0\u793A\u8BCD\uFF08\u652F\u6301 @file \u8BFB\u7EAF\u6587\u672C\uFF09").argument("<id>", "\u6D4B\u8BD5\u96C6 ID").requiredOption("--prompt <text|@file>", "\u63D0\u793A\u8BCD\u5B57\u9762\u91CF\u6216 @file \u8DEF\u5F84").action(async (id, opts) => {
4637
+ try {
4638
+ const prompt = readPromptInput(opts.prompt);
4639
+ const data = await setEvalPrompt(id, { prompt });
4640
+ formatOutput({ success: true, data }, program2.opts().table);
4641
+ } catch (err) {
4642
+ reportCaughtError(err);
4643
+ process.exit(toExitCode(err));
4644
+ }
4645
+ });
4646
+ registerCaseCommand(program2, testset);
4647
+ registerBatchCommand(program2, testset);
4648
+ registerRunCommand(program2, testset);
4649
+ registerRunCaseCommand(program2, testset);
4650
+ registerExportCommand(program2, testset);
4651
+ }
4652
+
3450
4653
  // src/commands/workspace.ts
3451
4654
  var DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
3452
4655
  function toISODate(d) {
@@ -3533,6 +4736,7 @@ registerMonitorCommand(program);
3533
4736
  registerRepairRecordCommand(program);
3534
4737
  registerOperationsRecordCommand(program);
3535
4738
  registerChangeConsumerCommand(program);
4739
+ registerTestsetCommand(program);
3536
4740
  process.on("uncaughtException", (err) => {
3537
4741
  outputError(3, err.message);
3538
4742
  process.exit(3);