openyida 2026.5.13-beta.0 → 2026.5.13
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 +13 -8
- package/bin/yida.js +19 -17
- package/lib/ai/ai.js +497 -0
- package/lib/app/create-form.js +18 -7
- package/lib/auth/login.js +25 -20
- package/lib/basic-info/basic-info.js +407 -0
- package/lib/core/command-manifest.js +6 -0
- package/lib/core/locales/ar.js +3 -1
- package/lib/core/locales/de.js +3 -1
- package/lib/core/locales/en.js +14 -12
- package/lib/core/locales/es.js +3 -1
- package/lib/core/locales/fr.js +3 -1
- package/lib/core/locales/hi.js +3 -1
- package/lib/core/locales/ja.js +9 -7
- package/lib/core/locales/ko.js +3 -1
- package/lib/core/locales/pt.js +3 -1
- package/lib/core/locales/vi.js +3 -1
- package/lib/core/locales/zh-HK.js +11 -9
- package/lib/core/locales/zh.js +14 -12
- package/lib/core/query-data.js +77 -26
- package/lib/core/utils.js +23 -10
- package/package.json +2 -1
- package/project/pages/src/demo-dingtalk-ai-solution-center.oyd.jsx +3960 -0
- package/project/prd/demo-dingtalk-ai-solution-center.md +425 -0
- package/scripts/e2e-real/skill-coverage.js +1 -0
- package/scripts/solution-center-runner.js +368 -0
- package/yida-skills/SKILL.md +14 -1
- package/yida-skills/skills/yida-app/SKILL.md +4 -3
- package/yida-skills/skills/yida-basic-info/SKILL.md +137 -0
- package/yida-skills/skills/yida-create-form-page/SKILL.md +4 -1
- package/yida-skills/skills/yida-create-process/SKILL.md +3 -2
- package/yida-skills/skills/yida-custom-page/SKILL.md +4 -3
- package/yida-skills/skills/yida-data-management/SKILL.md +12 -5
- package/yida-skills/skills/yida-process-rule/SKILL.md +2 -1
- package/yida-skills/skills/yida-process-rule/references/examples.md +10 -10
- package/yida-skills/skills/yida-report/SKILL.md +2 -0
- 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
|
|
@@ -294,6 +296,7 @@ Run `openyida --help` or `openyida <command> --help` for detailed usage.
|
|
|
294
296
|
| `openyida data <action> <resource> [args]` | Unified data management for forms, processes, tasks, and subforms |
|
|
295
297
|
| `openyida data check <appType> <formUuid> <rules.json>` | Detect anomalous process-form records |
|
|
296
298
|
| `openyida task-center <type> [options]` | Query todo, created, processed, CC, or proxy-submitted tasks |
|
|
299
|
+
| `openyida basic-info <overview\|commodity\|grant\|capacity\|quota\|abs-path\|dataflow\|i18n\|domain>` | Query organization basic info, capacity, quotas, fixed-domain records, and domain settings |
|
|
297
300
|
| `openyida get-permission <appType> <formUuid>` | Query form permission configuration |
|
|
298
301
|
| `openyida save-permission <appType> <formUuid> [options]` | Save form permission configuration |
|
|
299
302
|
| `openyida corp-manager <sub-command>` | Manage platform admins, sub-admins, app admins, and address book visibility |
|
|
@@ -329,6 +332,8 @@ Run `openyida --help` or `openyida <command> --help` for detailed usage.
|
|
|
329
332
|
| `openyida update` | Update OpenYida through npm |
|
|
330
333
|
| `openyida export-conversation [options]` | Export AI conversation history |
|
|
331
334
|
| `openyida flash-to-prd --file <path> --name "<project>"` | Convert flash notes or meeting notes into a PRD prompt |
|
|
335
|
+
| `openyida ai text --prompt "..."` | Call Yida's text generation AI API |
|
|
336
|
+
| `openyida ai image --file <image> --app-type APP_XXX` | Upload an image and call the image recognition connector |
|
|
332
337
|
| `openyida cdn-config` | Configure image upload to Aliyun OSS/CDN |
|
|
333
338
|
| `openyida cdn-upload <image-path>` | Upload an image to CDN |
|
|
334
339
|
| `openyida cdn-refresh [options]` | Refresh CDN cache |
|
package/bin/yida.js
CHANGED
|
@@ -123,7 +123,7 @@ function printHelp() {
|
|
|
123
123
|
console.log(`\n ${BOLD}${CYAN}${t('help.quickstart_title')}${RESET}`);
|
|
124
124
|
console.log(` ${DIM}${RESET} openyida login`);
|
|
125
125
|
console.log(` ${DIM}${RESET} openyida create-app "${t('help.quickstart_app_name')}"`);
|
|
126
|
-
console.log(` ${DIM}${RESET} openyida create-form create APP_XXX "${t('help.quickstart_form_name')}" fields.json`);
|
|
126
|
+
console.log(` ${DIM}${RESET} openyida create-form create APP_XXX "${t('help.quickstart_form_name')}" .cache/openyida/forms/fields.json`);
|
|
127
127
|
console.log(` ${DIM}${RESET} openyida dws contact user search --keyword "张三"`);
|
|
128
128
|
console.log('');
|
|
129
129
|
console.log(` ${DIM}${t('help.docs')} https://openyida.ai · https://github.com/openyida/openyida${RESET}`);
|
|
@@ -370,23 +370,13 @@ async function main() {
|
|
|
370
370
|
const result = checkLoginOnly({ includeSecrets: args.includes('--with-cookies') });
|
|
371
371
|
console.log(JSON.stringify(result, null, 2));
|
|
372
372
|
} else if (shouldUseCodexQrLogin(args)) {
|
|
373
|
-
const
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
} else {
|
|
377
|
-
const { startCodexQrLogin } = require('../lib/auth/qr-login');
|
|
378
|
-
const result = await startCodexQrLogin({ corpId: getArgValue(args, '--corp-id') });
|
|
379
|
-
printLoginResult(result);
|
|
380
|
-
}
|
|
373
|
+
const { startCodexQrLogin } = require('../lib/auth/qr-login');
|
|
374
|
+
const result = await startCodexQrLogin({ corpId: getArgValue(args, '--corp-id') });
|
|
375
|
+
printLoginResult(result);
|
|
381
376
|
} else if (args.includes('--browser')) {
|
|
382
|
-
const
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
} else {
|
|
386
|
-
const { interactiveLogin } = require('../lib/auth/login');
|
|
387
|
-
const result = interactiveLogin();
|
|
388
|
-
printLoginResult(result);
|
|
389
|
-
}
|
|
377
|
+
const { interactiveLogin } = require('../lib/auth/login');
|
|
378
|
+
const result = interactiveLogin({ force: true });
|
|
379
|
+
printLoginResult(result);
|
|
390
380
|
} else if (args.includes('--qoder') || args.includes('--wukong')) {
|
|
391
381
|
const { codexLogin } = require('../lib/auth/codex-login');
|
|
392
382
|
const result = await codexLogin({ tool: args.includes('--qoder') ? 'qoder' : 'wukong' });
|
|
@@ -665,6 +655,12 @@ async function main() {
|
|
|
665
655
|
break;
|
|
666
656
|
}
|
|
667
657
|
|
|
658
|
+
case 'basic-info': {
|
|
659
|
+
const { run: runBasicInfo } = require('../lib/basic-info/basic-info');
|
|
660
|
+
await runBasicInfo(args);
|
|
661
|
+
break;
|
|
662
|
+
}
|
|
663
|
+
|
|
668
664
|
case 'doctor': {
|
|
669
665
|
const { run } = require('../lib/core/doctor');
|
|
670
666
|
await run(args);
|
|
@@ -856,6 +852,12 @@ async function main() {
|
|
|
856
852
|
break;
|
|
857
853
|
}
|
|
858
854
|
|
|
855
|
+
case 'ai': {
|
|
856
|
+
const { run: runAI } = require('../lib/ai/ai');
|
|
857
|
+
await runAI(args);
|
|
858
|
+
break;
|
|
859
|
+
}
|
|
860
|
+
|
|
859
861
|
case 'integration': {
|
|
860
862
|
const subCommand = args[0];
|
|
861
863
|
const subArgs = args.slice(1); // 路由层消费 subCommand,传递剩余参数
|
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
|
+
};
|
package/lib/app/create-form.js
CHANGED
|
@@ -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
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
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;
|