@yizhuan-cli/cli 0.1.3 → 0.1.5-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -34,23 +34,47 @@ yizhuan config path
34
34
  yizhuan config show
35
35
  yizhuan execute --ability membership
36
36
  yizhuan execute --ability works --query "百家号昨天新增了哪些作品"
37
- yizhuan execute --ability accounts --params "{\"mode\":\"public_search\",\"platform\":\"toutiao\",\"keyword\":\"张三\"}"
38
- yizhuan execute --ability radar --params "{\"mode\":\"following\"}"
39
- yizhuan execute --ability radar --params "{\"mode\":\"timeline\",\"timeRange\":\"7d\"}"
40
- yizhuan execute --ability radar --params "{\"mode\":\"unfollow\",\"articleId\":\"205169870\"}"
37
+ yizhuan execute --ability hot_topics --platform douyin
38
+ yizhuan execute --ability hot_topics --params '{"platform":"douyin"}'
39
+ yizhuan execute --ability accounts --params '{"mode":"public_search","platform":"toutiao","keyword":"张三"}'
40
+ yizhuan execute --ability radar --params '{"mode":"following"}'
41
+ yizhuan execute --ability radar --mode timeline --timeRange 7d
42
+ yizhuan execute --ability radar --params '{"mode":"unfollow","articleId":"205169870"}'
43
+ ```
44
+
45
+ ### PowerShell 参数说明
46
+
47
+ PowerShell 对 JSON 双引号转义不友好。推荐以下写法:
48
+
49
+ ```powershell
50
+ # 最稳妥:独立参数
51
+ yizhuan execute --ability hot_topics --platform douyin
52
+
53
+ # 单引号包 JSON
54
+ yizhuan execute --ability hot_topics --params '{"platform":"douyin"}'
55
+
56
+ # key=value
57
+ yizhuan execute --ability hot_topics --params platform=douyin
58
+ ```
59
+
60
+ 请避免 bash 风格的:
61
+
62
+ ```powershell
63
+ # 在 PowerShell 中容易失败
64
+ yizhuan execute --ability hot_topics --params "{\"platform\":\"douyin\"}"
41
65
  ```
42
66
 
43
67
  ## 支持的典型能力
44
68
 
45
69
  - 作品查询:`yizhuan execute --ability works --query "AI 相关的热门作品有哪些"`
46
70
  - 作者查询:`yizhuan execute --ability accounts --query "查询张三这个作者"`
47
- - 作者全网搜索:`yizhuan execute --ability accounts --params "{\"mode\":\"public_search\",\"platform\":\"toutiao\",\"keyword\":\"张三\"}"`
48
- - 关注全网作者:`yizhuan execute --ability accounts --params "{\"mode\":\"follow_public_author\",\"platform\":\"toutiao\",\"keyword\":\"张三\",\"extAuthorId\":\"author-1\"}"`
49
- - 热点查询:`yizhuan execute --ability hot_topics --query "查询头条平台热点话题"`
50
- - 雷达查询:`yizhuan execute --ability radar --params "{\"mode\":\"following\"}"`
51
- - 竞品动态:`yizhuan execute --ability radar --params "{\"mode\":\"timeline\",\"timeRange\":\"7d\"}"`
52
- - 竞品动态筛选:`yizhuan execute --ability radar --params "{\"mode\":\"timeline\",\"timeRange\":\"7d\",\"platform\":\"baijiahao\",\"keyword\":\"AI\"}"`
53
- - 取消关注雷达作者:先查询关注列表拿到 `articleId`,再执行 `yizhuan execute --ability radar --params "{\"mode\":\"unfollow\",\"articleId\":\"205169870\"}"`
71
+ - 作者全网搜索:`yizhuan execute --ability accounts --params '{"mode":"public_search","platform":"toutiao","keyword":"张三"}'`
72
+ - 关注全网作者:`yizhuan execute --ability accounts --params '{"mode":"follow_public_author","platform":"toutiao","keyword":"张三","extAuthorId":"author-1"}'`
73
+ - 热点查询:`yizhuan execute --ability hot_topics --platform douyin`
74
+ - 雷达查询:`yizhuan execute --ability radar --params '{"mode":"following"}'`
75
+ - 竞品动态:`yizhuan execute --ability radar --mode timeline --timeRange 7d`
76
+ - 竞品动态筛选:`yizhuan execute --ability radar --params '{"mode":"timeline","timeRange":"7d","platform":"baijiahao","keyword":"AI"}'`
77
+ - 取消关注雷达作者:先查询关注列表拿到 `articleId`,再执行 `yizhuan execute --ability radar --params '{"mode":"unfollow","articleId":"205169870"}'`
54
78
  - 会员查询:`yizhuan execute --ability membership --query "我的会员什么时候到期"`
55
79
 
56
80
  如果你是通过 Codex、Cursor、Claude Code 这类 Agent 使用 CLI,也可以直接用自然语言提问,例如:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yizhuan-cli/cli",
3
- "version": "0.1.3",
3
+ "version": "0.1.5-beta.0",
4
4
  "description": "易撰命令行工具,用于通过 API Key 查询易撰真实数据。",
5
5
  "private": false,
6
6
  "type": "module",
@@ -14,7 +14,9 @@
14
14
  "scripts": {
15
15
  "start": "node src/index.js",
16
16
  "pack:dry-run": "npm pack --dry-run",
17
- "smoke": "node src/index.js --help && node src/index.js --version"
17
+ "smoke": "node src/index.js --help && node --test src/version.test.js",
18
+ "test:params": "node --test src/params.test.js",
19
+ "test:version": "node --test src/version.test.js"
18
20
  },
19
21
  "engines": {
20
22
  "node": ">=22"
package/src/index.js CHANGED
@@ -6,7 +6,7 @@ import { fileURLToPath } from 'node:url'
6
6
 
7
7
  const CLI_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
8
8
  const PACKAGE_PATH = path.join(CLI_DIR, 'package.json')
9
- const CONFIG_PATH = path.join(os.homedir(), '.config', 'yizhuan', 'config.json')
9
+ const CONFIG_PATH = process.env.YIZHUAN_CONFIG_PATH || path.join(os.homedir(), '.config', 'yizhuan', 'config.json')
10
10
  const ABILITIES = new Set([
11
11
  'works',
12
12
  'accounts',
@@ -25,22 +25,49 @@ const ABILITIES = new Set([
25
25
  'favorites_query'
26
26
  ])
27
27
 
28
+ const PARAM_FLAG_KEYS = [
29
+ 'platform',
30
+ 'mode',
31
+ 'keyword',
32
+ 'page',
33
+ 'pageSize',
34
+ 'timeRange',
35
+ 'date',
36
+ 'startDate',
37
+ 'endDate',
38
+ 'articleId',
39
+ 'authorId',
40
+ 'extAuthorId',
41
+ 'collectionType',
42
+ 'metric'
43
+ ]
44
+
28
45
  function printHelp() {
29
46
  console.log(`易撰 CLI
30
47
 
31
48
  Usage:
32
49
  yizhuan --help
33
50
  yizhuan --version
34
- yizhuan execute --ability <ability> [--query <text>] [--params <json>]
51
+ yizhuan execute --ability <ability> [--query <text>] [--params <json>] [--platform <name>]
52
+ yizhuan query --json '<request JSON>' [--format json|table|markdown]
53
+ yizhuan query --json-file <path> [--format json|table|markdown]
35
54
  yizhuan config path
36
55
  yizhuan config show
37
56
 
38
57
  Examples:
39
58
  yizhuan execute --ability works --query "百家号昨天新增了哪些作品"
40
- yizhuan execute --ability accounts --params "{\\"mode\\":\\"public_search\\",\\"platform\\":\\"toutiao\\",\\"keyword\\":\\"张三\\"}"
41
- yizhuan execute --ability radar --params "{\\"mode\\":\\"following\\"}"
42
- yizhuan execute --ability radar --params "{\\"mode\\":\\"timeline\\",\\"timeRange\\":\\"7d\\"}"
43
- yizhuan execute --ability radar --params "{\\"mode\\":\\"unfollow\\",\\"articleId\\":\\"205169870\\"}"
59
+ yizhuan execute --ability hot_topics --platform douyin
60
+ yizhuan execute --ability hot_topics --params '{"platform":"douyin"}'
61
+ yizhuan execute --ability accounts --params '{"mode":"public_search","platform":"toutiao","keyword":"张三"}'
62
+ yizhuan execute --ability radar --params '{"mode":"following"}'
63
+ yizhuan execute --ability radar --mode timeline --timeRange 7d
64
+ yizhuan execute --ability radar --params '{"mode":"unfollow","articleId":"205169870"}'
65
+
66
+ Notes:
67
+ - PowerShell 推荐优先使用 --platform / --mode 等独立参数,或单引号 JSON:--params '{"platform":"douyin"}'
68
+ - 所有 Shell 都可用 --json-file request.json,避免 JSON 引号被终端改写
69
+ - bash/zsh 也可用:--params '{"platform":"douyin"}' 或 --platform douyin
70
+ - 兼容 key=value:--params platform=douyin
44
71
 
45
72
  Abilities:
46
73
  works 作品查询
@@ -53,6 +80,7 @@ Abilities:
53
80
 
54
81
  Config:
55
82
  ${CONFIG_PATH}
83
+ 可通过环境变量 YIZHUAN_CONFIG_PATH 指定独立配置文件
56
84
  `)
57
85
  }
58
86
 
@@ -65,22 +93,84 @@ function readPackageVersion() {
65
93
  }
66
94
  }
67
95
 
68
- function parseArgs(argv) {
96
+ function isBalancedJsonLike(text) {
97
+ let brace = 0
98
+ let bracket = 0
99
+ let quote = null
100
+ let escaped = false
101
+
102
+ for (const char of text) {
103
+ if (escaped) {
104
+ escaped = false
105
+ continue
106
+ }
107
+ if (char === '\\') {
108
+ escaped = true
109
+ continue
110
+ }
111
+ if (quote) {
112
+ if (char === quote) quote = null
113
+ continue
114
+ }
115
+ if (char === '"' || char === "'") {
116
+ quote = char
117
+ continue
118
+ }
119
+ if (char === '{') brace += 1
120
+ if (char === '}') brace -= 1
121
+ if (char === '[') bracket += 1
122
+ if (char === ']') bracket -= 1
123
+ if (brace < 0 || bracket < 0) return false
124
+ }
125
+
126
+ return brace === 0 && bracket === 0 && quote === null
127
+ }
128
+
129
+ function readFlagValue(argv, startIndex, key) {
130
+ let index = startIndex
131
+ let raw = argv[index]
132
+ if (raw == null) return { value: true, nextIndex: startIndex - 1 }
133
+
134
+ if (key === 'params' && /[{\[]/.test(raw)) {
135
+ let joined = raw
136
+ while (!isBalancedJsonLike(joined) && index + 1 < argv.length && !String(argv[index + 1]).startsWith('--')) {
137
+ index += 1
138
+ joined += argv[index]
139
+ }
140
+ return { value: joined, nextIndex: index }
141
+ }
142
+
143
+ return { value: raw, nextIndex: index }
144
+ }
145
+
146
+ export function parseArgs(argv) {
69
147
  const args = { _: [] }
70
148
  for (let index = 0; index < argv.length; index += 1) {
71
149
  const value = argv[index]
72
- if (value.startsWith('--')) {
73
- const key = value.slice(2)
74
- const next = argv[index + 1]
75
- if (!next || next.startsWith('--')) {
76
- args[key] = true
77
- } else {
78
- args[key] = next
79
- index += 1
80
- }
81
- } else {
150
+ if (!value.startsWith('--')) {
82
151
  args._.push(value)
152
+ continue
153
+ }
154
+
155
+ const body = value.slice(2)
156
+ const eqIndex = body.indexOf('=')
157
+ if (eqIndex > 0) {
158
+ const key = body.slice(0, eqIndex)
159
+ const raw = body.slice(eqIndex + 1)
160
+ args[key] = raw
161
+ continue
162
+ }
163
+
164
+ const key = body
165
+ const next = argv[index + 1]
166
+ if (!next || next.startsWith('--')) {
167
+ args[key] = true
168
+ continue
83
169
  }
170
+
171
+ const read = readFlagValue(argv, index + 1, key)
172
+ args[key] = read.value
173
+ index = read.nextIndex
84
174
  }
85
175
  return args
86
176
  }
@@ -101,13 +191,107 @@ function readConfig() {
101
191
  }
102
192
  }
103
193
 
104
- function parseParams(value) {
105
- if (!value) return undefined
106
- try {
107
- return JSON.parse(value)
108
- } catch {
109
- throw new Error('--params 必须是 JSON,例如:--params "{\\"page\\":1}"')
194
+ function stripWrappingQuotes(text) {
195
+ const value = String(text).trim()
196
+ if (
197
+ (value.startsWith('"') && value.endsWith('"')) ||
198
+ (value.startsWith("'") && value.endsWith("'"))
199
+ ) {
200
+ return value.slice(1, -1)
201
+ }
202
+ return value
203
+ }
204
+
205
+ function coerceScalar(value) {
206
+ const text = stripWrappingQuotes(value).trim()
207
+ if (text === 'true') return true
208
+ if (text === 'false') return false
209
+ if (text === 'null') return null
210
+ if (text !== '' && /^-?\d+(\.\d+)?$/.test(text)) return Number(text)
211
+ return text
212
+ }
213
+
214
+ function sanitizeJsonLike(text) {
215
+ return String(text)
216
+ .replace(/^\uFEFF/, '')
217
+ .replace(/[\u201C\u201D]/g, '"')
218
+ .replace(/[\u2018\u2019]/g, "'")
219
+ .replace(/\\+"/g, '"')
220
+ .replace(/\\+'/g, "'")
221
+ .replace(/\\([{}:[\]:,])/g, '$1')
222
+ .replace(/\\/g, '')
223
+ .replace(/\s+/g, ' ')
224
+ .trim()
225
+ }
226
+
227
+ function parseLooseObject(text) {
228
+ let body = String(text).trim()
229
+ if (!body) return undefined
230
+
231
+ if ((body.startsWith('{') && body.endsWith('}')) || (body.startsWith('[') && body.endsWith(']'))) {
232
+ body = body.slice(1, -1).trim()
110
233
  }
234
+ if (!body) return {}
235
+
236
+ const pairs = body
237
+ .split(',')
238
+ .map((part) => part.trim())
239
+ .filter(Boolean)
240
+ if (!pairs.length) return undefined
241
+
242
+ const result = {}
243
+ for (const pair of pairs) {
244
+ const matched = pair.match(
245
+ /^(?:["']?)([A-Za-z_][\w-]*)(?:["']?)\s*[:=]\s*(.+)$/
246
+ )
247
+ if (!matched) return undefined
248
+ result[matched[1]] = coerceScalar(matched[2])
249
+ }
250
+ return result
251
+ }
252
+
253
+ export function parseParams(value) {
254
+ if (value == null || value === true || value === false) return undefined
255
+ if (typeof value === 'object' && !Array.isArray(value)) return value
256
+
257
+ const original = String(value).trim()
258
+ if (!original) return undefined
259
+
260
+ const candidates = [original, sanitizeJsonLike(original), original.replace(/\\/g, '')]
261
+ for (const candidate of candidates) {
262
+ try {
263
+ const parsed = JSON.parse(candidate)
264
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) return parsed
265
+ } catch {
266
+ // continue
267
+ }
268
+ }
269
+
270
+ for (const candidate of candidates) {
271
+ const loose = parseLooseObject(candidate)
272
+ if (loose) return loose
273
+ }
274
+
275
+ throw new Error(
276
+ '--params 必须是 JSON 或 key=value。PowerShell 推荐:--platform douyin 或 --params \'{"platform":"douyin"}\''
277
+ )
278
+ }
279
+
280
+ function assignFlagParams(params, args) {
281
+ for (const key of PARAM_FLAG_KEYS) {
282
+ if (args[key] == null || args[key] === true) continue
283
+ if (params[key] != null) continue
284
+ params[key] = coerceScalar(args[key])
285
+ }
286
+ return params
287
+ }
288
+
289
+ export function buildExecuteParams(args) {
290
+ const params = {
291
+ ...(parseParams(args.params) || {})
292
+ }
293
+ assignFlagParams(params, args)
294
+ return Object.keys(params).length > 0 ? params : undefined
111
295
  }
112
296
 
113
297
  async function execute(args) {
@@ -127,8 +311,8 @@ async function execute(args) {
127
311
  body: JSON.stringify({
128
312
  json: {
129
313
  ability,
130
- query: args.query,
131
- params: parseParams(args.params)
314
+ query: typeof args.query === 'string' ? args.query : undefined,
315
+ params: buildExecuteParams(args)
132
316
  }
133
317
  })
134
318
  })
@@ -142,6 +326,33 @@ async function execute(args) {
142
326
  console.log(JSON.stringify(payload.result?.data?.json ?? payload, null, 2))
143
327
  }
144
328
 
329
+ function renderQuery(payload, format) {
330
+ if (format === 'json' || !format) return JSON.stringify(payload, null, 2)
331
+ if (payload.status === 'error') return `${payload.error.code}: ${payload.error.message}`
332
+ const items = payload.data?.items
333
+ if (!Array.isArray(items)) return JSON.stringify(payload, null, 2)
334
+ if (format === 'markdown') return items.map((item, index) => `| ${index + 1} | ${item.title || item.name || item.nickname || ''} |`).join('\n')
335
+ return items.map((item, index) => `${index + 1}\t${item.title || item.name || item.nickname || ''}`).join('\n')
336
+ }
337
+
338
+ async function query(args) {
339
+ const jsonText = args.jsonFile
340
+ ? fs.readFileSync(path.resolve(String(args.jsonFile)), 'utf8')
341
+ : args.json === '-'
342
+ ? fs.readFileSync(0, 'utf8')
343
+ : args.json || args.params
344
+ const request = parseParams(jsonText)
345
+ if (!request) throw new Error('query 必须提供 --json 请求对象')
346
+ const config = readConfig()
347
+ const response = await fetch(`${config.apiBaseUrl}/api/agent.query`, {
348
+ method: 'POST', headers: { authorization: config.apiKey, 'content-type': 'application/json', 'x-device-type': 'pc' },
349
+ body: JSON.stringify({ json: request })
350
+ })
351
+ const text = await response.text()
352
+ if (!response.ok) throw new Error(`请求失败 HTTP ${response.status}:${text}`)
353
+ console.log(renderQuery(JSON.parse(text).result?.data?.json ?? JSON.parse(text), args.format || 'json'))
354
+ }
355
+
145
356
  async function main() {
146
357
  const args = parseArgs(process.argv.slice(2))
147
358
  const command = args._[0]
@@ -171,11 +382,27 @@ async function main() {
171
382
  await execute(args)
172
383
  return
173
384
  }
385
+ if (command === 'query') {
386
+ await query(args)
387
+ return
388
+ }
174
389
 
175
390
  throw new Error(`未知命令:${command}`)
176
391
  }
177
392
 
178
- main().catch((error) => {
179
- console.error(error.message)
180
- process.exit(1)
181
- })
393
+ const isDirectRun = (() => {
394
+ const entry = process.argv[1]
395
+ if (!entry) return false
396
+ try {
397
+ return fs.realpathSync(path.resolve(entry)) === fs.realpathSync(fileURLToPath(import.meta.url))
398
+ } catch {
399
+ return false
400
+ }
401
+ })()
402
+
403
+ if (isDirectRun) {
404
+ main().catch((error) => {
405
+ console.error(error.message)
406
+ process.exit(1)
407
+ })
408
+ }
@@ -0,0 +1,54 @@
1
+ import test from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { buildExecuteParams, parseArgs, parseParams } from './index.js'
4
+
5
+ test('parseParams accepts standard JSON', () => {
6
+ assert.deepEqual(parseParams('{"platform":"douyin"}'), { platform: 'douyin' })
7
+ })
8
+
9
+ test('parseParams repairs PowerShell-mangled escaped JSON fragments', () => {
10
+ assert.deepEqual(parseParams('{\\platform\\:\\douyin\\}'), { platform: 'douyin' })
11
+ assert.deepEqual(parseParams('{"platform":"douyin"}'.replaceAll('"', '\\"')), {
12
+ platform: 'douyin'
13
+ })
14
+ })
15
+
16
+ test('parseParams accepts key=value and loose objects', () => {
17
+ assert.deepEqual(parseParams('platform=douyin'), { platform: 'douyin' })
18
+ assert.deepEqual(parseParams('{platform:douyin,mode:list}'), {
19
+ platform: 'douyin',
20
+ mode: 'list'
21
+ })
22
+ })
23
+
24
+ test('buildExecuteParams merges top-level flags', () => {
25
+ assert.deepEqual(
26
+ buildExecuteParams({
27
+ params: '{"mode":"timeline"}',
28
+ platform: 'douyin',
29
+ timeRange: '7d'
30
+ }),
31
+ {
32
+ mode: 'timeline',
33
+ platform: 'douyin',
34
+ timeRange: '7d'
35
+ }
36
+ )
37
+ })
38
+
39
+ test('buildExecuteParams prefers explicit params keys over flags', () => {
40
+ assert.deepEqual(
41
+ buildExecuteParams({
42
+ params: 'platform=weibo',
43
+ platform: 'douyin'
44
+ }),
45
+ { platform: 'weibo' }
46
+ )
47
+ })
48
+
49
+ test('parseArgs preserves query JSON supplied after --json', () => {
50
+ assert.deepEqual(
51
+ parseArgs(['query', '--json', '{"intent":"author_search","platform":"douyin"}']),
52
+ { _: ['query'], json: '{"intent":"author_search","platform":"douyin"}' }
53
+ )
54
+ })
@@ -0,0 +1,22 @@
1
+ import assert from 'node:assert/strict'
2
+ import { execFileSync } from 'node:child_process'
3
+ import fs from 'node:fs'
4
+ import path from 'node:path'
5
+ import test from 'node:test'
6
+ import { fileURLToPath } from 'node:url'
7
+
8
+ const CLI_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
9
+ const ENTRY_PATH = path.join(CLI_DIR, 'src', 'index.js')
10
+ const PACKAGE_PATH = path.join(CLI_DIR, 'package.json')
11
+ const expectedVersion = JSON.parse(fs.readFileSync(PACKAGE_PATH, 'utf8')).version
12
+
13
+ for (const flag of ['--version', '-v']) {
14
+ test(`${flag} prints the package version`, () => {
15
+ const output = execFileSync(process.execPath, [ENTRY_PATH, flag], {
16
+ encoding: 'utf8'
17
+ }).trim()
18
+
19
+ assert.equal(output, expectedVersion)
20
+ assert.notEqual(output, '')
21
+ })
22
+ }