openyida 2026.5.12 → 2026.5.13-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +13 -8
  2. package/bin/yida.js +31 -17
  3. package/lib/ai/ai.js +497 -0
  4. package/lib/app/create-form.js +18 -7
  5. package/lib/auth/cdp-browser-login.js +14 -15
  6. package/lib/auth/login.js +33 -34
  7. package/lib/auth/org.js +8 -8
  8. package/lib/auth/qr-login.js +5 -12
  9. package/lib/core/command-manifest.js +7 -0
  10. package/lib/core/doctor.js +11 -4
  11. package/lib/core/env-manager.js +154 -18
  12. package/lib/core/locales/ar.js +2 -0
  13. package/lib/core/locales/de.js +2 -0
  14. package/lib/core/locales/en.js +14 -12
  15. package/lib/core/locales/es.js +2 -0
  16. package/lib/core/locales/fr.js +2 -0
  17. package/lib/core/locales/hi.js +2 -0
  18. package/lib/core/locales/ja.js +2 -0
  19. package/lib/core/locales/ko.js +2 -0
  20. package/lib/core/locales/pt.js +2 -0
  21. package/lib/core/locales/vi.js +2 -0
  22. package/lib/core/locales/zh-HK.js +2 -0
  23. package/lib/core/locales/zh.js +9 -7
  24. package/lib/core/query-data.js +77 -26
  25. package/lib/core/utils.js +23 -10
  26. package/lib/dingtalk/dingtalk-link.js +141 -0
  27. package/package.json +2 -1
  28. package/project/pages/src/demo-dingtalk-ai-solution-center.oyd.jsx +3776 -0
  29. package/project/prd/demo-dingtalk-ai-solution-center.md +425 -0
  30. package/scripts/solution-center-runner.js +368 -0
  31. package/yida-skills/SKILL.md +13 -1
  32. package/yida-skills/skills/yida-app/SKILL.md +4 -3
  33. package/yida-skills/skills/yida-create-form-page/SKILL.md +4 -1
  34. package/yida-skills/skills/yida-create-process/SKILL.md +3 -2
  35. package/yida-skills/skills/yida-custom-page/SKILL.md +4 -3
  36. package/yida-skills/skills/yida-data-management/SKILL.md +12 -5
  37. package/yida-skills/skills/yida-process-rule/SKILL.md +2 -1
  38. package/yida-skills/skills/yida-process-rule/references/examples.md +10 -10
  39. package/yida-skills/skills/yida-report/SKILL.md +2 -0
  40. package/yida-skills/skills/yida-report/references/examples.md +2 -2
package/README.md CHANGED
@@ -167,8 +167,8 @@ openyida/
167
167
  openyida create-app "CRM"
168
168
  openyida create-app --name "CRM" --desc "Customer management" --theme deepBlue
169
169
  openyida app-list --size 20
170
- openyida create-form create APP_XXX "Customer" fields.json
171
- openyida create-form update APP_XXX FORM_XXX changes.json
170
+ openyida create-form create APP_XXX "Customer" .cache/openyida/forms/customer-fields.json
171
+ openyida create-form update APP_XXX FORM_XXX .cache/openyida/forms/customer-changes.json
172
172
  openyida get-schema APP_XXX FORM_XXX
173
173
  openyida get-schema APP_XXX --all --output-dir .cache/schemas
174
174
  ```
@@ -177,7 +177,7 @@ openyida get-schema APP_XXX --all --output-dir .cache/schemas
177
177
 
178
178
  ```bash
179
179
  openyida create-page APP_XXX "Dashboard" --mode dashboard
180
- openyida generate-page product-homepage --spec page.json --output pages/src/home.oyd.jsx --compile
180
+ openyida generate-page product-homepage --spec .cache/openyida/page-specs/home.json --output pages/src/home.oyd.jsx --compile
181
181
  openyida generate-page todo-mvc --output pages/src/todo-mvc.oyd.jsx --compile
182
182
  openyida check-page pages/src/home.oyd.jsx
183
183
  openyida compile pages/src/home.oyd.jsx
@@ -190,14 +190,16 @@ Built-in templates currently include `product-homepage` for product/portal pages
190
190
  ### Workflow, Data, and Permissions
191
191
 
192
192
  ```bash
193
- openyida create-process APP_XXX "Purchase Request" fields.json process.json
194
- openyida configure-process APP_XXX FORM_XXX process.json
195
- openyida process preview APP_XXX PROC_INST_XXX --output process.html
193
+ openyida create-process APP_XXX "Purchase Request" .cache/openyida/process/fields.json .cache/openyida/process/process.json
194
+ openyida configure-process APP_XXX FORM_XXX .cache/openyida/process/process.json
195
+ openyida process preview APP_XXX PROC_INST_XXX --output .cache/openyida/process/process.html
196
196
  openyida data query form APP_XXX FORM_XXX --page 1 --size 20
197
+ openyida data create form APP_XXX FORM_XXX --data-file .cache/openyida/data-import/record.json
197
198
  openyida get-permission APP_XXX FORM_XXX
198
199
  ```
199
200
 
200
201
  When creating or updating test data with `openyida data`, Yida date fields must use 13-digit millisecond timestamps, for example `"dateField_xxx": 1719705600000`. Do not submit `YYYY-MM-DD` strings for `DateField` or `CascadeDateField` values.
202
+ Temporary JSON, CSV, and one-off import scripts should live under `.cache/openyida/` so generated run artifacts do not clutter the repository root.
201
203
 
202
204
  ### Real Environment E2E
203
205
 
@@ -239,8 +241,8 @@ Use `npm run test:e2e:real:cleanup` to list recorded disposable resources. OpenY
239
241
  openyida connector smart-create --curl "curl https://api.example.com/users"
240
242
  openyida connector list
241
243
  openyida integration create APP_XXX FORM_XXX "Sync customer data"
242
- openyida create-report APP_XXX "Sales Dashboard" charts.json
243
- openyida append-chart APP_XXX REPORT_XXX chart.json
244
+ openyida create-report APP_XXX "Sales Dashboard" .cache/openyida/reports/charts.json
245
+ openyida append-chart APP_XXX REPORT_XXX .cache/openyida/reports/chart.json
244
246
  ```
245
247
 
246
248
  ## CLI Reference
@@ -316,6 +318,7 @@ Run `openyida --help` or `openyida <command> --help` for detailed usage.
316
318
  | `openyida integration enable <appType> <formUuid> <processCode>` | Enable an automation flow |
317
319
  | `openyida integration disable <appType> <formUuid> <processCode>` | Disable an automation flow |
318
320
  | `openyida dws <command> [args]` | Access DingTalk CLI capabilities such as contacts, calendar, todo, and approval |
321
+ | `openyida dingtalk-link <url> [--target fullScreen] [--legacy-scheme] [--json]` | Generate DingTalk AppLink URLs for opening pages in DingTalk; use `--legacy-scheme` only when old `dingtalk://` links are required |
319
322
 
320
323
  ### Utilities
321
324
 
@@ -328,6 +331,8 @@ Run `openyida --help` or `openyida <command> --help` for detailed usage.
328
331
  | `openyida update` | Update OpenYida through npm |
329
332
  | `openyida export-conversation [options]` | Export AI conversation history |
330
333
  | `openyida flash-to-prd --file <path> --name "<project>"` | Convert flash notes or meeting notes into a PRD prompt |
334
+ | `openyida ai text --prompt "..."` | Call Yida's text generation AI API |
335
+ | `openyida ai image --file <image> --app-type APP_XXX` | Upload an image and call the image recognition connector |
331
336
  | `openyida cdn-config` | Configure image upload to Aliyun OSS/CDN |
332
337
  | `openyida cdn-upload <image-path>` | Upload an image to CDN |
333
338
  | `openyida cdn-refresh [options]` | Refresh CDN cache |
package/bin/yida.js CHANGED
@@ -262,6 +262,15 @@ function shouldUseAgentLogin(cliArgs) {
262
262
  return isAgentConversationEnvironment();
263
263
  }
264
264
 
265
+ function shouldUsePlaywrightFallbackInAgentLogin() {
266
+ const { detectActiveTool } = require('../lib/core/utils');
267
+ const activeTool = detectActiveTool();
268
+ if (activeTool && activeTool.tool === 'wukong') {
269
+ return true;
270
+ }
271
+ return process.env.OPENYIDA_AGENT_PLAYWRIGHT_FALLBACK === '1';
272
+ }
273
+
265
274
  function shouldUseCodexQrLogin(cliArgs) {
266
275
  if (cliArgs.includes('--codex-qr') || cliArgs.includes('--agent-qr')) {return true;}
267
276
  return false;
@@ -361,23 +370,13 @@ async function main() {
361
370
  const result = checkLoginOnly({ includeSecrets: args.includes('--with-cookies') });
362
371
  console.log(JSON.stringify(result, null, 2));
363
372
  } else if (shouldUseCodexQrLogin(args)) {
364
- const cachedResult = checkLoginOnly({ includeSecrets: true });
365
- if (cachedResult.status === 'ok') {
366
- printLoginResult(cachedResult);
367
- } else {
368
- const { startCodexQrLogin } = require('../lib/auth/qr-login');
369
- const result = await startCodexQrLogin({ corpId: getArgValue(args, '--corp-id') });
370
- printLoginResult(result);
371
- }
373
+ const { startCodexQrLogin } = require('../lib/auth/qr-login');
374
+ const result = await startCodexQrLogin({ corpId: getArgValue(args, '--corp-id') });
375
+ printLoginResult(result);
372
376
  } else if (args.includes('--browser')) {
373
- const cachedResult = checkLoginOnly({ includeSecrets: true });
374
- if (cachedResult.status === 'ok') {
375
- printLoginResult(cachedResult);
376
- } else {
377
- const { interactiveLogin } = require('../lib/auth/login');
378
- const result = interactiveLogin();
379
- printLoginResult(result);
380
- }
377
+ const { interactiveLogin } = require('../lib/auth/login');
378
+ const result = interactiveLogin({ force: true });
379
+ printLoginResult(result);
381
380
  } else if (args.includes('--qoder') || args.includes('--wukong')) {
382
381
  const { codexLogin } = require('../lib/auth/codex-login');
383
382
  const result = await codexLogin({ tool: args.includes('--qoder') ? 'qoder' : 'wukong' });
@@ -392,7 +391,9 @@ async function main() {
392
391
  printLoginResult(cachedResult);
393
392
  } else {
394
393
  const { interactiveLogin } = require('../lib/auth/login');
395
- const browserResult = interactiveLogin({ playwrightFallback: false });
394
+ const browserResult = interactiveLogin({
395
+ playwrightFallback: shouldUsePlaywrightFallbackInAgentLogin(),
396
+ });
396
397
  if (browserResult) {
397
398
  printLoginResult(browserResult);
398
399
  } else {
@@ -845,6 +846,12 @@ async function main() {
845
846
  break;
846
847
  }
847
848
 
849
+ case 'ai': {
850
+ const { run: runAI } = require('../lib/ai/ai');
851
+ await runAI(args);
852
+ break;
853
+ }
854
+
848
855
  case 'integration': {
849
856
  const subCommand = args[0];
850
857
  const subArgs = args.slice(1); // 路由层消费 subCommand,传递剩余参数
@@ -879,6 +886,13 @@ async function main() {
879
886
  await runDws(args);
880
887
  break;
881
888
  }
889
+
890
+ case 'dingtalk-link': {
891
+ const { run: runDingTalkLink } = require('../lib/dingtalk/dingtalk-link');
892
+ await runDingTalkLink(args);
893
+ break;
894
+ }
895
+
882
896
  case 'export-conversation': {
883
897
  const { exportConversation } = require('../lib/conversation/export-conversation');
884
898
  // 解析选项
package/lib/ai/ai.js ADDED
@@ -0,0 +1,497 @@
1
+ /**
2
+ * ai.js - 宜搭 AI 能力命令
3
+ *
4
+ * 支持:
5
+ * openyida ai text --prompt "..." 调用 txtFromAI 文生文
6
+ * openyida ai image --file ./image.png 上传图片并调用识图连接器
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+ const crypto = require('crypto');
14
+ const querystring = require('querystring');
15
+ const {
16
+ loadCookieData,
17
+ triggerLogin,
18
+ resolveBaseUrl,
19
+ findProjectRoot,
20
+ httpGet,
21
+ httpPost,
22
+ requestWithAutoLogin,
23
+ } = require('../core/utils');
24
+ const { warn } = require('../core/chalk');
25
+
26
+ const DEFAULT_MAX_TOKENS = 3000;
27
+ const DEFAULT_IMAGE_CONNECTOR = {
28
+ connectorId: 'Http_2aa221179eef4c128de666c5b9c8df1b',
29
+ actionId: 'flowerrecognize',
30
+ connection: 2391,
31
+ };
32
+
33
+ const IMAGE_MIME_TYPES = {
34
+ '.png': 'image/png',
35
+ '.jpg': 'image/jpeg',
36
+ '.jpeg': 'image/jpeg',
37
+ '.gif': 'image/gif',
38
+ '.webp': 'image/webp',
39
+ '.bmp': 'image/bmp',
40
+ };
41
+
42
+ function printHelp() {
43
+ console.log(`
44
+ 用法:
45
+ openyida ai text --prompt "提示词" [--max-tokens 3000] [--json]
46
+ openyida ai text --file prompt.txt [--json]
47
+ openyida ai image --file ./image.png --app-type APP_XXX [--json]
48
+ openyida ai image --image-url https://... [--json]
49
+
50
+ 子命令:
51
+ text 调用宜搭 /query/intelligent/txtFromAI.json
52
+ image 上传本地图片并调用识图连接器;也可直接传入 --image-url
53
+
54
+ image 选项:
55
+ --app-type <APP_XXX> 上传图片使用的宜搭应用 ID;未传时尝试读取当前 project/config.json 或 Cookie
56
+ --form-uuid <FORM_XXX> 可选,上传回调关联的表单 ID
57
+ --connector-id <id> 识图 HTTP 连接器 ID,默认使用 HAR 中的植物识别连接器
58
+ --action-id <id> 识图动作 ID,默认 flowerrecognize
59
+ --connection <id> 识图连接账号 ID,默认 2391
60
+ --baike / --no-baike 是否返回百科信息,默认 --baike
61
+ --base-url <url> 覆盖宜搭域名,例如 https://demo.aliwork.com
62
+ `);
63
+ }
64
+
65
+ function parseArgs(args) {
66
+ const parsed = {
67
+ subCommand: args[0],
68
+ prompt: '',
69
+ file: '',
70
+ imageUrl: '',
71
+ maxTokens: DEFAULT_MAX_TOKENS,
72
+ json: false,
73
+ appType: '',
74
+ formUuid: '',
75
+ connectorId: DEFAULT_IMAGE_CONNECTOR.connectorId,
76
+ actionId: DEFAULT_IMAGE_CONNECTOR.actionId,
77
+ connection: DEFAULT_IMAGE_CONNECTOR.connection,
78
+ baike: true,
79
+ baseUrl: '',
80
+ help: false,
81
+ };
82
+
83
+ for (let i = 1; i < args.length; i++) {
84
+ const arg = args[i];
85
+ if (arg === '--help' || arg === '-h') {
86
+ parsed.help = true;
87
+ } else if ((arg === '--prompt' || arg === '-p') && args[i + 1]) {
88
+ parsed.prompt = args[++i];
89
+ } else if ((arg === '--file' || arg === '-f') && args[i + 1]) {
90
+ parsed.file = args[++i];
91
+ } else if (arg === '--image-url' && args[i + 1]) {
92
+ parsed.imageUrl = args[++i];
93
+ } else if (arg === '--max-tokens' && args[i + 1]) {
94
+ parsed.maxTokens = parseInt(args[++i], 10) || DEFAULT_MAX_TOKENS;
95
+ } else if (arg === '--json') {
96
+ parsed.json = true;
97
+ } else if (arg === '--app-type' && args[i + 1]) {
98
+ parsed.appType = args[++i];
99
+ } else if (arg === '--form-uuid' && args[i + 1]) {
100
+ parsed.formUuid = args[++i];
101
+ } else if (arg === '--connector-id' && args[i + 1]) {
102
+ parsed.connectorId = args[++i];
103
+ } else if (arg === '--action-id' && args[i + 1]) {
104
+ parsed.actionId = args[++i];
105
+ } else if (arg === '--connection' && args[i + 1]) {
106
+ parsed.connection = parseInt(args[++i], 10) || args[i];
107
+ } else if (arg === '--baike') {
108
+ parsed.baike = true;
109
+ } else if (arg === '--no-baike') {
110
+ parsed.baike = false;
111
+ } else if (arg === '--base-url' && args[i + 1]) {
112
+ parsed.baseUrl = args[++i].replace(/\/+$/, '');
113
+ }
114
+ }
115
+
116
+ return parsed;
117
+ }
118
+
119
+ function readStdin() {
120
+ return new Promise((resolve, reject) => {
121
+ if (process.stdin.isTTY) {
122
+ resolve('');
123
+ return;
124
+ }
125
+ let data = '';
126
+ process.stdin.setEncoding('utf-8');
127
+ process.stdin.on('data', (chunk) => { data += chunk; });
128
+ process.stdin.on('end', () => resolve(data));
129
+ process.stdin.on('error', reject);
130
+ });
131
+ }
132
+
133
+ async function readPrompt(options) {
134
+ if (options.prompt) {
135
+ return options.prompt;
136
+ }
137
+ if (options.file) {
138
+ const absolutePath = path.resolve(options.file);
139
+ if (!fs.existsSync(absolutePath)) {
140
+ throw new Error(`文件不存在: ${absolutePath}`);
141
+ }
142
+ return fs.readFileSync(absolutePath, 'utf-8');
143
+ }
144
+ return readStdin();
145
+ }
146
+
147
+ function getAuthRef(options) {
148
+ let cookieData = loadCookieData();
149
+ if (!cookieData || !cookieData.cookies || !cookieData.csrf_token) {
150
+ cookieData = triggerLogin();
151
+ }
152
+ if (!cookieData || !cookieData.cookies || !cookieData.csrf_token) {
153
+ throw new Error('未获取到有效宜搭登录态,请先执行 openyida login');
154
+ }
155
+ return {
156
+ csrfToken: cookieData.csrf_token || '',
157
+ cookies: cookieData.cookies || [],
158
+ baseUrl: options.baseUrl || resolveBaseUrl(cookieData),
159
+ cookieData,
160
+ };
161
+ }
162
+
163
+ function getCookieValue(authRef, name) {
164
+ const matched = (authRef.cookies || []).filter(cookie => cookie.name === name);
165
+ return matched.length ? matched[0].value : '';
166
+ }
167
+
168
+ function inferAppType(options, authRef) {
169
+ if (options.appType) {
170
+ return options.appType;
171
+ }
172
+
173
+ const projectRoot = findProjectRoot();
174
+ const configPath = path.join(projectRoot, 'config.json');
175
+ if (fs.existsSync(configPath)) {
176
+ try {
177
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
178
+ if (config.appType) {
179
+ return config.appType;
180
+ }
181
+ } catch {
182
+ // ignore invalid local config and continue with cookie fallback
183
+ }
184
+ }
185
+
186
+ return getCookieValue(authRef, 'tianshu_app_type');
187
+ }
188
+
189
+ function getSuccessContent(response, fallbackMessage) {
190
+ if (!response || !response.success) {
191
+ throw new Error(response && response.errorMsg ? response.errorMsg : fallbackMessage);
192
+ }
193
+ return response.content;
194
+ }
195
+
196
+ async function callTextFromAI(prompt, options, authRef) {
197
+ const response = await requestWithAutoLogin((auth) => {
198
+ const postData = querystring.stringify({
199
+ _csrf_token: auth.csrfToken,
200
+ prompt,
201
+ maxTokens: String(options.maxTokens || DEFAULT_MAX_TOKENS),
202
+ skill: 'ToText',
203
+ });
204
+ return httpPost(auth.baseUrl, '/query/intelligent/txtFromAI.json', postData, auth.cookies);
205
+ }, authRef);
206
+
207
+ const content = getSuccessContent(response, 'AI 接口调用失败');
208
+
209
+ return {
210
+ success: true,
211
+ content: content && content.content ? content.content : '',
212
+ raw: response,
213
+ };
214
+ }
215
+
216
+ function getMimeType(filePath) {
217
+ const ext = path.extname(filePath).toLowerCase();
218
+ return IMAGE_MIME_TYPES[ext] || 'application/octet-stream';
219
+ }
220
+
221
+ function createObjectName(appType, filePath) {
222
+ const ext = path.extname(filePath) || '.png';
223
+ const now = new Date();
224
+ const monthDay = (now.getMonth() + 1) + '-' + now.getDate();
225
+ const id = crypto.randomUUID ? crypto.randomUUID().toUpperCase() : crypto.randomBytes(16).toString('hex').toUpperCase();
226
+ return appType + '/' + now.getFullYear() + '/' + monthDay + '/' + id + ext;
227
+ }
228
+
229
+ async function getOssSign(filePath, appType, authRef) {
230
+ const stat = fs.statSync(filePath);
231
+ const fileName = path.basename(filePath);
232
+ const contentType = getMimeType(filePath);
233
+ const objectName = createObjectName(appType, filePath);
234
+
235
+ return requestWithAutoLogin((auth) => {
236
+ return httpGet(auth.baseUrl, '/ossSign', {
237
+ scene: 'ImageField',
238
+ _api: 'nattyFetch',
239
+ _mock: 'false',
240
+ _csrf_token: auth.csrfToken,
241
+ appType,
242
+ fileName,
243
+ fileSize: String(stat.size),
244
+ contentType,
245
+ isOpen: 'n',
246
+ newContext: 'y',
247
+ objectName,
248
+ procInstId: '',
249
+ businessType: '',
250
+ accelerate: 'y',
251
+ _stamp: String(Date.now()),
252
+ }, auth.cookies);
253
+ }, authRef);
254
+ }
255
+
256
+ async function postToOss(filePath, signContent) {
257
+ if (typeof fetch !== 'function' || typeof FormData !== 'function' || typeof Blob !== 'function') {
258
+ throw new Error('当前 Node.js 环境缺少 fetch/FormData,请使用 Node.js 18+');
259
+ }
260
+
261
+ const fileName = path.basename(filePath);
262
+ const fileBuffer = fs.readFileSync(filePath);
263
+ const form = new FormData();
264
+ form.append('accessid', signContent.accessid);
265
+ form.append('key', signContent.objectName);
266
+ form.append('policy', signContent.policy);
267
+ form.append('OSSAccessKeyId', signContent.accessid);
268
+ form.append('signature', signContent.signature);
269
+ form.append('expire', signContent.expire);
270
+ form.append('appType', signContent.appType);
271
+ form.append('Content-Disposition', 'attachment; filename=' + fileName);
272
+ form.append('file', new Blob([fileBuffer], { type: getMimeType(filePath) }), fileName);
273
+
274
+ const response = await fetch(signContent.host, {
275
+ method: 'POST',
276
+ body: form,
277
+ });
278
+
279
+ if (!response.ok && response.status !== 204) {
280
+ const body = await response.text();
281
+ throw new Error('OSS 上传失败: HTTP ' + response.status + ' ' + body.slice(0, 160));
282
+ }
283
+
284
+ return {
285
+ status: response.status,
286
+ requestId: response.headers.get('x-oss-request-id') || '',
287
+ };
288
+ }
289
+
290
+ async function convertToPublicImageUrl(downloadUrl, authRef) {
291
+ const response = await requestWithAutoLogin((auth) => {
292
+ return httpGet(auth.baseUrl, '/aliyun/sdk/upload2Oss.json', {
293
+ imageUrl: downloadUrl,
294
+ _csrf_token: auth.csrfToken,
295
+ }, auth.cookies);
296
+ }, authRef);
297
+
298
+ if (!response || !response.success) {
299
+ throw new Error(response && response.errorMsg ? response.errorMsg : '图片 URL 转换失败');
300
+ }
301
+ return response.content;
302
+ }
303
+
304
+ async function uploadCallback(filePath, appType, formUuid, signContent, ossResult, authRef) {
305
+ if (!formUuid) {
306
+ return { skipped: true };
307
+ }
308
+
309
+ const stat = fs.statSync(filePath);
310
+ return requestWithAutoLogin((auth) => {
311
+ const postData = querystring.stringify({
312
+ _csrf_token: auth.csrfToken,
313
+ appType,
314
+ fileName: path.basename(filePath),
315
+ fileSize: String(stat.size),
316
+ objectName: signContent.objectName,
317
+ formUuid,
318
+ procInstId: '',
319
+ ossRequestId: ossResult.requestId || '',
320
+ businessType: 'inst',
321
+ });
322
+ return httpPost(auth.baseUrl, '/query/attach/uploadCallBack.json', postData, auth.cookies);
323
+ }, authRef);
324
+ }
325
+
326
+ async function uploadImageForAI(filePath, options, authRef) {
327
+ const absolutePath = path.resolve(filePath);
328
+ if (!fs.existsSync(absolutePath)) {
329
+ throw new Error(`图片不存在: ${absolutePath}`);
330
+ }
331
+
332
+ const appType = inferAppType(options, authRef);
333
+ if (!appType) {
334
+ throw new Error('上传图片需要 appType,请传入 --app-type APP_XXX');
335
+ }
336
+
337
+ const signResponse = await getOssSign(absolutePath, appType, authRef);
338
+ const signContent = getSuccessContent(signResponse, '获取 OSS 上传签名失败');
339
+ if (!signContent) {
340
+ throw new Error('获取 OSS 上传签名失败');
341
+ }
342
+
343
+ const ossResult = await postToOss(absolutePath, signContent);
344
+ const publicImageUrl = await convertToPublicImageUrl(signContent.downloadUrl, authRef);
345
+ const callbackResult = await uploadCallback(absolutePath, appType, options.formUuid, signContent, ossResult, authRef);
346
+
347
+ return {
348
+ appType,
349
+ fileName: path.basename(absolutePath),
350
+ fileSize: fs.statSync(absolutePath).size,
351
+ contentType: getMimeType(absolutePath),
352
+ objectName: signContent.objectName,
353
+ downloadUrl: signContent.downloadUrl,
354
+ previewUrl: signContent.previewUrl,
355
+ imageUrl: publicImageUrl,
356
+ ossRequestId: ossResult.requestId,
357
+ callback: callbackResult && callbackResult.skipped ? 'skipped' : 'ok',
358
+ };
359
+ }
360
+
361
+ async function invokeImageRecognition(imageUrl, options, authRef) {
362
+ const inputs = {
363
+ path: {},
364
+ query: {
365
+ PageIndex: '1',
366
+ PageSize: '50',
367
+ KeyWord: '',
368
+ },
369
+ header: {},
370
+ body: {
371
+ image: imageUrl,
372
+ baike: options.baike ? '1' : '0',
373
+ },
374
+ };
375
+ const serviceInfo = {
376
+ connectorInfo: {
377
+ connectorId: options.connectorId,
378
+ actionId: options.actionId,
379
+ type: 'httpConnector',
380
+ connection: options.connection,
381
+ },
382
+ };
383
+
384
+ const response = await requestWithAutoLogin((auth) => {
385
+ const postData = querystring.stringify({
386
+ inputs: JSON.stringify(inputs),
387
+ serviceInfo: JSON.stringify(serviceInfo),
388
+ _csrf_token: auth.csrfToken,
389
+ });
390
+ return httpPost(auth.baseUrl, '/query/publicService/invokeService.json', postData, auth.cookies);
391
+ }, authRef);
392
+
393
+ if (!response || !response.success) {
394
+ throw new Error(response && response.errorMsg ? response.errorMsg : '识图服务调用失败');
395
+ }
396
+
397
+ return response.content && response.content.serviceReturnValue
398
+ ? response.content.serviceReturnValue
399
+ : response.content;
400
+ }
401
+
402
+ function normalizeImageResult(serviceReturnValue) {
403
+ const list = serviceReturnValue && Array.isArray(serviceReturnValue.result)
404
+ ? serviceReturnValue.result
405
+ : [];
406
+ return list.map(item => ({
407
+ name: item.name || '',
408
+ score: typeof item.score === 'number' ? item.score : Number(item.score) || 0,
409
+ confidence: Math.round((typeof item.score === 'number' ? item.score : Number(item.score) || 0) * 10000) / 100,
410
+ baikeInfo: item.baike_info || item.baikeInfo || {},
411
+ raw: item,
412
+ }));
413
+ }
414
+
415
+ function printImageSummary(result) {
416
+ console.log('图片 URL: ' + result.imageUrl);
417
+ if (!result.recognition.length) {
418
+ console.log('识别结果: 无结果');
419
+ return;
420
+ }
421
+ console.log('识别结果:');
422
+ result.recognition.forEach((item, index) => {
423
+ console.log(` ${index + 1}. ${item.name || '未知'} ${item.confidence}%`);
424
+ });
425
+ }
426
+
427
+ async function runText(options, authRef) {
428
+ const prompt = await readPrompt(options);
429
+ if (!prompt.trim()) {
430
+ throw new Error('请通过 --prompt、--file 或 stdin 提供提示词');
431
+ }
432
+ const output = await callTextFromAI(prompt, options, authRef);
433
+ if (options.json) {
434
+ console.log(JSON.stringify(output, null, 2));
435
+ } else {
436
+ console.log(output.content);
437
+ }
438
+ }
439
+
440
+ async function runImage(options, authRef) {
441
+ let upload = null;
442
+ let imageUrl = options.imageUrl;
443
+
444
+ if (!imageUrl) {
445
+ if (!options.file) {
446
+ throw new Error('请通过 --file 上传图片,或通过 --image-url 传入图片 URL');
447
+ }
448
+ upload = await uploadImageForAI(options.file, options, authRef);
449
+ imageUrl = upload.imageUrl;
450
+ }
451
+
452
+ const serviceReturnValue = await invokeImageRecognition(imageUrl, options, authRef);
453
+ const result = {
454
+ success: true,
455
+ imageUrl,
456
+ upload,
457
+ recognition: normalizeImageResult(serviceReturnValue),
458
+ raw: serviceReturnValue,
459
+ };
460
+
461
+ if (options.json) {
462
+ console.log(JSON.stringify(result, null, 2));
463
+ } else {
464
+ printImageSummary(result);
465
+ }
466
+ }
467
+
468
+ async function run(args) {
469
+ const options = parseArgs(args);
470
+ if (!options.subCommand || options.help || options.subCommand === '--help' || options.subCommand === '-h') {
471
+ printHelp();
472
+ return;
473
+ }
474
+
475
+ const authRef = getAuthRef(options);
476
+ if (options.subCommand === 'text' || options.subCommand === 'txt') {
477
+ await runText(options, authRef);
478
+ return;
479
+ }
480
+ if (options.subCommand === 'image' || options.subCommand === 'vision' || options.subCommand === 'image-recognize') {
481
+ await runImage(options, authRef);
482
+ return;
483
+ }
484
+
485
+ warn(`未知的 ai 子命令: ${options.subCommand}`);
486
+ printHelp();
487
+ process.exit(1);
488
+ }
489
+
490
+ module.exports = {
491
+ run,
492
+ callTextFromAI,
493
+ uploadImageForAI,
494
+ invokeImageRecognition,
495
+ normalizeImageResult,
496
+ parseArgs,
497
+ };
@@ -2187,16 +2187,27 @@ async function requestWithAutoLogin(requestFn, authRef) {
2187
2187
  // 307:csrf_token 过期,刷新后重试
2188
2188
  if (result && result.__csrfExpired) {
2189
2189
  const refreshedData = refreshCsrfToken();
2190
- authRef.cookieData = refreshedData;
2191
- authRef.csrfToken = refreshedData.csrf_token;
2192
- authRef.cookies = refreshedData.cookies;
2193
- authRef.baseUrl = resolveBaseUrl(refreshedData);
2194
- info(t('common.csrf_refreshed'));
2195
- result = await requestFn(authRef);
2190
+ if (refreshedData && refreshedData.cookies && refreshedData.csrf_token) {
2191
+ authRef.cookieData = refreshedData;
2192
+ authRef.csrfToken = refreshedData.csrf_token;
2193
+ authRef.cookies = refreshedData.cookies;
2194
+ authRef.baseUrl = resolveBaseUrl(refreshedData);
2195
+ info(t('common.csrf_refreshed'));
2196
+ result = await requestFn(authRef);
2197
+ } else {
2198
+ result = { __needLogin: true };
2199
+ }
2196
2200
  }
2197
2201
  // 302/301:登录态失效,重新登录后重试
2198
2202
  if (result && result.__needLogin) {
2199
- const newCookieData = triggerLogin();
2203
+ const newCookieData = triggerLogin({ force: true });
2204
+ if (!newCookieData || !newCookieData.cookies || !newCookieData.csrf_token) {
2205
+ return {
2206
+ success: false,
2207
+ __needLogin: true,
2208
+ errorMsg: t('common.login_expired', 'openyida login --qr / openyida login --browser'),
2209
+ };
2210
+ }
2200
2211
  authRef.cookieData = newCookieData;
2201
2212
  authRef.csrfToken = newCookieData.csrf_token;
2202
2213
  authRef.cookies = newCookieData.cookies;