@opentiny/tiny-robot-cli 0.5.2-alpha.11 → 0.5.2-alpha.12
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 +2 -2
- package/bin/commands/add.js +105 -289
- package/package.json +4 -3
- package/templates/chat/.env.example +1 -4
- package/templates/chat/README.md +10 -8
- package/templates/chat/package.json +1 -1
- package/templates/chat/src/TinyRobotChat.vue +132 -52
- package/templates/chat/src/main.ts +0 -3
- package/templates/chat/src/tiny-robot-chat/components/ComposerTools.vue +72 -0
- package/templates/chat/src/{composables → tiny-robot-chat/composables}/useWindow.ts +18 -8
- package/templates/chat/src/tiny-robot-chat/config/chat-runtime.ts +26 -0
- package/templates/chat/src/{config → tiny-robot-chat/config}/chat-ui.ts +4 -22
- package/templates/chat/src/components/ComposerTools.vue +0 -99
- package/templates/chat/src/config/chat-runtime.ts +0 -57
- package/templates/chat/src/index.css +0 -116
- /package/templates/chat/src/{components → tiny-robot-chat/components}/WindowHeader.vue +0 -0
package/README.md
CHANGED
|
@@ -15,14 +15,14 @@ npx @opentiny/tiny-robot-cli add chat
|
|
|
15
15
|
- `-t, --template <name>`: template name, currently supports `basic`
|
|
16
16
|
- `-h, --help`: show help
|
|
17
17
|
|
|
18
|
-
`add chat`
|
|
18
|
+
`add chat` adds a self-contained floating Chat component, including its runtime, UI configuration, composables, component-scoped styles, and supporting components. It updates these OpenTiny dependencies to `0.5.2-alpha.10`:
|
|
19
19
|
|
|
20
20
|
- `@opentiny/tiny-robot`
|
|
21
21
|
- `@opentiny/tiny-robot-chat`
|
|
22
22
|
- `@opentiny/tiny-robot-kit`
|
|
23
23
|
- `@opentiny/tiny-robot-svgs`
|
|
24
24
|
|
|
25
|
-
Configure the generated `.env`
|
|
25
|
+
It also adds `@vueuse/core@13.9.0`. The command does not modify the application entry file or create `src/index.css`. Configure a private environment file from the generated `.env.example` before starting the project; the generated example only includes the Qwen and DeepSeek API key variables. Their API URL variables remain optional overrides for proxies, private gateways, or other regions. The CLI never creates or modifies `.env`.
|
|
26
26
|
|
|
27
27
|
## Template Documentation
|
|
28
28
|
|
package/bin/commands/add.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { confirm, select } from '@inquirer/prompts'
|
|
2
2
|
import { Argument } from 'commander'
|
|
3
3
|
import fs from 'node:fs'
|
|
4
4
|
import path from 'node:path'
|
|
@@ -21,23 +21,13 @@ import {
|
|
|
21
21
|
} from '../utils.js'
|
|
22
22
|
|
|
23
23
|
const TARGET_VERSION = '0.5.2-alpha.10'
|
|
24
|
+
const VUEUSE_VERSION = '13.9.0'
|
|
24
25
|
const DEPENDENCIES = {
|
|
25
26
|
'@opentiny/tiny-robot': TARGET_VERSION,
|
|
26
27
|
'@opentiny/tiny-robot-chat': TARGET_VERSION,
|
|
27
28
|
'@opentiny/tiny-robot-kit': TARGET_VERSION,
|
|
28
29
|
'@opentiny/tiny-robot-svgs': TARGET_VERSION,
|
|
29
|
-
|
|
30
|
-
const STYLE_IMPORTS = [
|
|
31
|
-
"import '@opentiny/tiny-robot/dist/style.css'",
|
|
32
|
-
"import '@opentiny/tiny-robot-chat/dist/style.css'",
|
|
33
|
-
]
|
|
34
|
-
|
|
35
|
-
function logUnavailable(label) {
|
|
36
|
-
logSkip(`${label} could not be applied`)
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function logSkippedSelection(label) {
|
|
40
|
-
logSkip(`${label} change was not selected`)
|
|
30
|
+
'@vueuse/core': VUEUSE_VERSION,
|
|
41
31
|
}
|
|
42
32
|
|
|
43
33
|
async function resolveTargetPackage(cwd) {
|
|
@@ -51,7 +41,6 @@ async function resolveTargetPackage(cwd) {
|
|
|
51
41
|
}
|
|
52
42
|
|
|
53
43
|
const workspacePatterns = findWorkspacePackages(workspaceRoot)
|
|
54
|
-
|
|
55
44
|
const packageDirs = listPackages(workspaceRoot, workspacePatterns)
|
|
56
45
|
|
|
57
46
|
invariant(packageDirs.length > 0, 'no packages found in workspace.')
|
|
@@ -72,7 +61,6 @@ async function resolveTargetPackage(cwd) {
|
|
|
72
61
|
}
|
|
73
62
|
|
|
74
63
|
console.error('Error: no package.json found.')
|
|
75
|
-
|
|
76
64
|
process.exit(1)
|
|
77
65
|
}
|
|
78
66
|
|
|
@@ -88,62 +76,8 @@ function writePackageJson(pkgPath, pkg) {
|
|
|
88
76
|
fs.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`)
|
|
89
77
|
}
|
|
90
78
|
|
|
91
|
-
function findMainEntry(targetDir) {
|
|
92
|
-
for (const file of ['src/main.ts', 'src/main.js']) {
|
|
93
|
-
const fullPath = path.join(targetDir, file)
|
|
94
|
-
|
|
95
|
-
if (fs.existsSync(fullPath)) {
|
|
96
|
-
return fullPath
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
return null
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function ensureStyleImport(mainFile) {
|
|
104
|
-
const content = fs.readFileSync(mainFile, 'utf-8')
|
|
105
|
-
const lines = content.split('\n')
|
|
106
|
-
let inserted = false
|
|
107
|
-
|
|
108
|
-
for (const styleImport of STYLE_IMPORTS) {
|
|
109
|
-
if (lines.some((line) => line.trim() === styleImport)) {
|
|
110
|
-
continue
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
let lastImportIndex = -1
|
|
114
|
-
|
|
115
|
-
for (let i = 0; i < lines.length; i++) {
|
|
116
|
-
if (/^\s*import\s/.test(lines[i])) {
|
|
117
|
-
lastImportIndex = i
|
|
118
|
-
continue
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
if (lastImportIndex !== -1) {
|
|
122
|
-
break
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
if (lastImportIndex === -1) {
|
|
127
|
-
lines.unshift(styleImport)
|
|
128
|
-
} else {
|
|
129
|
-
lines.splice(lastImportIndex + 1, 0, styleImport)
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
inserted = true
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
if (inserted) {
|
|
136
|
-
fs.writeFileSync(mainFile, lines.join('\n'))
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
return {
|
|
140
|
-
type: inserted ? 'inserted' : 'skipped',
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
79
|
function insertDependencyOrdered(dependencies, name, version) {
|
|
145
80
|
const next = {}
|
|
146
|
-
|
|
147
81
|
let inserted = false
|
|
148
82
|
|
|
149
83
|
for (const [key, value] of Object.entries(dependencies)) {
|
|
@@ -167,7 +101,6 @@ function ensureDependency(pkg, name, targetVersion) {
|
|
|
167
101
|
|
|
168
102
|
const currentVersion = pkg.dependencies[name]
|
|
169
103
|
|
|
170
|
-
// 1. 不存在 => 新增(使用 ordered insert)
|
|
171
104
|
if (!currentVersion) {
|
|
172
105
|
pkg.dependencies = insertDependencyOrdered(pkg.dependencies, name, targetVersion)
|
|
173
106
|
|
|
@@ -177,22 +110,19 @@ function ensureDependency(pkg, name, targetVersion) {
|
|
|
177
110
|
}
|
|
178
111
|
}
|
|
179
112
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
pkg.dependencies[name] = targetVersion
|
|
185
|
-
|
|
113
|
+
if (
|
|
114
|
+
currentVersion.startsWith('workspace:') ||
|
|
115
|
+
semver.satisfies(targetVersion, currentVersion, { includePrerelease: true })
|
|
116
|
+
) {
|
|
186
117
|
return {
|
|
187
|
-
type: '
|
|
188
|
-
from: currentVersion,
|
|
189
|
-
to: targetVersion,
|
|
118
|
+
type: 'skipped',
|
|
190
119
|
}
|
|
191
120
|
}
|
|
192
121
|
|
|
193
|
-
// 3. 完全一致 => 跳过
|
|
194
122
|
return {
|
|
195
|
-
type: '
|
|
123
|
+
type: 'conflict',
|
|
124
|
+
from: currentVersion,
|
|
125
|
+
to: targetVersion,
|
|
196
126
|
}
|
|
197
127
|
}
|
|
198
128
|
|
|
@@ -202,278 +132,168 @@ function printDependencyResult(result, name) {
|
|
|
202
132
|
logSuccess(`Added ${name}@${result.to}`)
|
|
203
133
|
break
|
|
204
134
|
|
|
205
|
-
case 'updated':
|
|
206
|
-
logSuccess(`Updated ${name} from ${result.from} to ${result.to}`)
|
|
207
|
-
break
|
|
208
|
-
|
|
209
135
|
case 'skipped':
|
|
210
136
|
logSkip(`${name} already satisfies required version`)
|
|
211
137
|
break
|
|
212
138
|
}
|
|
213
139
|
}
|
|
214
140
|
|
|
215
|
-
function
|
|
216
|
-
const mainEntry = findMainEntry(targetDir)
|
|
141
|
+
function getGeneratedFiles(targetDir) {
|
|
217
142
|
const templateSource = getTemplateFile('src')
|
|
143
|
+
const supportTarget = path.join(targetDir, 'src/tiny-robot-chat')
|
|
218
144
|
|
|
219
145
|
return [
|
|
220
146
|
{
|
|
221
147
|
label: 'TinyRobotChat.vue',
|
|
148
|
+
source: path.join(templateSource, 'TinyRobotChat.vue'),
|
|
222
149
|
target: path.join(targetDir, 'src/TinyRobotChat.vue'),
|
|
223
|
-
|
|
224
|
-
action(fileExists) {
|
|
225
|
-
return fileExists ? 'overwrite' : 'create'
|
|
226
|
-
},
|
|
227
|
-
},
|
|
228
|
-
{
|
|
229
|
-
label: 'main entry style imports',
|
|
230
|
-
target: mainEntry,
|
|
231
|
-
action() {
|
|
232
|
-
return 'modify'
|
|
233
|
-
},
|
|
150
|
+
type: 'file',
|
|
234
151
|
},
|
|
235
152
|
{
|
|
236
153
|
label: 'Chat UI components',
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
return 'merge'
|
|
241
|
-
},
|
|
154
|
+
source: path.join(templateSource, 'tiny-robot-chat/components'),
|
|
155
|
+
target: path.join(supportTarget, 'components'),
|
|
156
|
+
type: 'directory',
|
|
242
157
|
},
|
|
243
158
|
{
|
|
244
159
|
label: 'Chat composables',
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
return 'merge'
|
|
249
|
-
},
|
|
160
|
+
source: path.join(templateSource, 'tiny-robot-chat/composables'),
|
|
161
|
+
target: path.join(supportTarget, 'composables'),
|
|
162
|
+
type: 'directory',
|
|
250
163
|
},
|
|
251
164
|
{
|
|
252
165
|
label: 'Chat configuration',
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
return 'merge'
|
|
257
|
-
},
|
|
258
|
-
},
|
|
259
|
-
{
|
|
260
|
-
label: 'Chat app styles',
|
|
261
|
-
target: path.join(targetDir, 'src/index.css'),
|
|
262
|
-
template: path.join(templateSource, 'index.css'),
|
|
263
|
-
action(fileExists) {
|
|
264
|
-
return fileExists ? 'overwrite' : 'create'
|
|
265
|
-
},
|
|
266
|
-
},
|
|
267
|
-
{
|
|
268
|
-
label: '.env',
|
|
269
|
-
target: path.join(targetDir, '.env'),
|
|
270
|
-
template: getTemplateFile('.env.example'),
|
|
271
|
-
action(fileExists) {
|
|
272
|
-
return fileExists ? 'modify' : 'create'
|
|
273
|
-
},
|
|
274
|
-
},
|
|
275
|
-
|
|
276
|
-
{
|
|
277
|
-
label: 'package.json',
|
|
278
|
-
target: path.join(targetDir, 'package.json'),
|
|
279
|
-
action() {
|
|
280
|
-
return 'modify'
|
|
281
|
-
},
|
|
166
|
+
source: path.join(templateSource, 'tiny-robot-chat/config'),
|
|
167
|
+
target: path.join(supportTarget, 'config'),
|
|
168
|
+
type: 'directory',
|
|
282
169
|
},
|
|
283
170
|
]
|
|
284
171
|
}
|
|
285
172
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
const exists = file.target ? fs.existsSync(file.target) : false
|
|
291
|
-
|
|
292
|
-
const disabled = !file.target
|
|
293
|
-
|
|
294
|
-
const descriptions = {
|
|
295
|
-
'TinyRobotChat.vue': 'integrate TinyRobot chat component',
|
|
296
|
-
'main entry style imports': 'import TinyRobot styles',
|
|
297
|
-
'Chat UI components': 'copy Chat UI components',
|
|
298
|
-
'Chat composables': 'copy Chat composables',
|
|
299
|
-
'Chat configuration': 'copy Chat runtime and UI configuration',
|
|
300
|
-
'Chat app styles': 'copy Chat application styles',
|
|
301
|
-
'.env': 'add environment variables',
|
|
302
|
-
'package.json': 'add TinyRobot dependencies',
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
const description = descriptions[file.label]
|
|
173
|
+
function findCopyConflicts(source, target) {
|
|
174
|
+
if (!fs.existsSync(target)) {
|
|
175
|
+
return []
|
|
176
|
+
}
|
|
306
177
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
? `${file.action(false)} ${file.label} — ${description} (main.ts/js not found)`
|
|
310
|
-
: `${file.action(exists)} ${file.label} — ${description}`,
|
|
178
|
+
const sourceStat = fs.statSync(source)
|
|
179
|
+
const targetStat = fs.statSync(target)
|
|
311
180
|
|
|
312
|
-
|
|
181
|
+
if (!sourceStat.isDirectory()) {
|
|
182
|
+
return [target]
|
|
183
|
+
}
|
|
313
184
|
|
|
314
|
-
|
|
185
|
+
if (!targetStat.isDirectory()) {
|
|
186
|
+
return [target]
|
|
187
|
+
}
|
|
315
188
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
}),
|
|
189
|
+
return fs.readdirSync(source, { withFileTypes: true }).flatMap((entry) => {
|
|
190
|
+
return findCopyConflicts(path.join(source, entry.name), path.join(target, entry.name))
|
|
319
191
|
})
|
|
320
192
|
}
|
|
321
193
|
|
|
322
|
-
function
|
|
323
|
-
|
|
324
|
-
}
|
|
194
|
+
export function applyChatFeature(targetDir) {
|
|
195
|
+
const pkgPath = path.join(targetDir, 'package.json')
|
|
325
196
|
|
|
326
|
-
|
|
327
|
-
invariant(type === 'chat', `unsupported feature: ${type}`)
|
|
197
|
+
invariant(fs.existsSync(pkgPath), 'package.json not found.')
|
|
328
198
|
|
|
329
|
-
const
|
|
199
|
+
const generatedFiles = getGeneratedFiles(targetDir)
|
|
200
|
+
const conflicts = generatedFiles.flatMap((file) => findCopyConflicts(file.source, file.target))
|
|
330
201
|
|
|
331
|
-
|
|
202
|
+
if (conflicts.length > 0) {
|
|
203
|
+
const conflictPaths = conflicts.map((file) => path.relative(targetDir, file)).join(', ')
|
|
332
204
|
|
|
333
|
-
|
|
334
|
-
logSkip('No changes selected.')
|
|
335
|
-
return
|
|
205
|
+
throw new Error(`Chat feature files already exist: ${conflictPaths}`)
|
|
336
206
|
}
|
|
337
207
|
|
|
338
|
-
const
|
|
339
|
-
|
|
340
|
-
})
|
|
341
|
-
|
|
342
|
-
const envFile = files.find((f) => {
|
|
343
|
-
return f.label === '.env'
|
|
344
|
-
})
|
|
345
|
-
|
|
346
|
-
invariant(componentFile, 'TinyRobotChat.vue config missing.')
|
|
347
|
-
|
|
348
|
-
invariant(envFile, '.env config missing.')
|
|
349
|
-
|
|
350
|
-
console.log('\nChange Results\n')
|
|
351
|
-
|
|
352
|
-
let needsManualStyleImport = false
|
|
353
|
-
let envChanged = false
|
|
354
|
-
let dependencyChanged = false
|
|
208
|
+
const pkg = readPackageJson(pkgPath)
|
|
209
|
+
const dependencyResults = []
|
|
355
210
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
logSuccess(`Copied ${componentFile.label}`)
|
|
359
|
-
} else {
|
|
360
|
-
logSkippedSelection(componentFile.label)
|
|
211
|
+
for (const [name, version] of Object.entries(DEPENDENCIES)) {
|
|
212
|
+
dependencyResults.push({ name, result: ensureDependency(pkg, name, version) })
|
|
361
213
|
}
|
|
362
214
|
|
|
363
|
-
|
|
364
|
-
if (!isSelected(selectedFiles, file.label)) {
|
|
365
|
-
logSkippedSelection(file.label)
|
|
366
|
-
continue
|
|
367
|
-
}
|
|
215
|
+
const dependencyConflicts = dependencyResults.filter(({ result }) => result.type === 'conflict')
|
|
368
216
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
}
|
|
217
|
+
if (dependencyConflicts.length > 0) {
|
|
218
|
+
const conflictDetails = dependencyConflicts
|
|
219
|
+
.map(({ name, result }) => `${name}: found ${result.from}, required ${result.to}`)
|
|
220
|
+
.join('; ')
|
|
374
221
|
|
|
375
|
-
|
|
222
|
+
throw new Error(`Dependency version conflict: ${conflictDetails}`)
|
|
376
223
|
}
|
|
377
224
|
|
|
378
|
-
const
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
if (!mainFile?.target) {
|
|
383
|
-
needsManualStyleImport = true
|
|
384
|
-
|
|
385
|
-
logUnavailable('main entry style imports (main.ts/js not found)')
|
|
386
|
-
} else {
|
|
387
|
-
if (isSelected(selectedFiles, mainFile.label)) {
|
|
388
|
-
const result = ensureStyleImport(mainFile.target)
|
|
389
|
-
|
|
390
|
-
switch (result.type) {
|
|
391
|
-
case 'inserted':
|
|
392
|
-
logSuccess('Inserted TinyRobot style import')
|
|
393
|
-
break
|
|
394
|
-
|
|
395
|
-
case 'skipped':
|
|
396
|
-
logSkip('TinyRobot style import already exists')
|
|
397
|
-
break
|
|
398
|
-
}
|
|
225
|
+
for (const file of generatedFiles) {
|
|
226
|
+
if (file.type === 'file') {
|
|
227
|
+
copyFile(file.source, file.target)
|
|
399
228
|
} else {
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
logSkippedSelection(mainFile.label)
|
|
229
|
+
copyDirectory(file.source, file.target)
|
|
403
230
|
}
|
|
404
231
|
}
|
|
405
232
|
|
|
406
|
-
|
|
407
|
-
const envResult = mergeEnvFile(envFile.template, envFile.target)
|
|
233
|
+
const envResult = mergeEnvFile(getTemplateFile('.env.example'), path.join(targetDir, '.env.example'))
|
|
408
234
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
235
|
+
if (dependencyResults.some(({ result }) => result.type !== 'skipped')) {
|
|
236
|
+
writePackageJson(pkgPath, pkg)
|
|
237
|
+
}
|
|
412
238
|
|
|
413
|
-
|
|
414
|
-
|
|
239
|
+
return {
|
|
240
|
+
generatedFiles,
|
|
241
|
+
envResult,
|
|
242
|
+
dependencyResults,
|
|
243
|
+
}
|
|
244
|
+
}
|
|
415
245
|
|
|
416
|
-
|
|
417
|
-
|
|
246
|
+
async function addFeature(targetDir, type) {
|
|
247
|
+
invariant(type === 'chat', `unsupported feature: ${type}`)
|
|
418
248
|
|
|
419
|
-
|
|
420
|
-
|
|
249
|
+
const shouldApply = await confirm({
|
|
250
|
+
message: `Add the complete TinyRobot chat feature to ${targetDir}?`,
|
|
251
|
+
default: true,
|
|
252
|
+
})
|
|
421
253
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
}
|
|
426
|
-
} else {
|
|
427
|
-
logSkippedSelection('.env')
|
|
254
|
+
if (!shouldApply) {
|
|
255
|
+
logSkip('No changes applied.')
|
|
256
|
+
return
|
|
428
257
|
}
|
|
429
258
|
|
|
430
|
-
const
|
|
259
|
+
const result = applyChatFeature(targetDir)
|
|
431
260
|
|
|
432
|
-
|
|
261
|
+
console.log('\nChange Results\n')
|
|
433
262
|
|
|
434
|
-
const
|
|
263
|
+
for (const file of result.generatedFiles) {
|
|
264
|
+
logSuccess(`Copied ${file.label}`)
|
|
265
|
+
}
|
|
435
266
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
267
|
+
switch (result.envResult.type) {
|
|
268
|
+
case 'created':
|
|
269
|
+
logSuccess('Created .env.example')
|
|
270
|
+
break
|
|
439
271
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
272
|
+
case 'merged':
|
|
273
|
+
logSuccess(`Added ${result.envResult.added} variables to .env.example`)
|
|
274
|
+
break
|
|
443
275
|
|
|
444
|
-
|
|
445
|
-
|
|
276
|
+
case 'skipped':
|
|
277
|
+
logSkip('.env.example already contains required variables')
|
|
278
|
+
break
|
|
279
|
+
}
|
|
446
280
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
logSkippedSelection('package.json')
|
|
281
|
+
for (const { name, result: dependencyResult } of result.dependencyResults) {
|
|
282
|
+
printDependencyResult(dependencyResult, name)
|
|
450
283
|
}
|
|
451
284
|
|
|
452
285
|
console.log(`\nSuccessfully added "${type}" feature to ${targetDir}`)
|
|
453
286
|
|
|
454
287
|
printNextSteps({
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
288
|
+
envChanged: result.envResult.type !== 'skipped',
|
|
289
|
+
dependencyChanged: result.dependencyResults.some(({ result: dependencyResult }) => {
|
|
290
|
+
return dependencyResult.type !== 'skipped'
|
|
291
|
+
}),
|
|
458
292
|
})
|
|
459
293
|
}
|
|
460
294
|
|
|
461
|
-
function printNextSteps({
|
|
462
|
-
const steps = [
|
|
463
|
-
|
|
464
|
-
if (needsManualStyleImport) {
|
|
465
|
-
steps.push(
|
|
466
|
-
[
|
|
467
|
-
'Import TinyRobot styles in your application entry file.',
|
|
468
|
-
'',
|
|
469
|
-
'Example:',
|
|
470
|
-
'',
|
|
471
|
-
...STYLE_IMPORTS.map((styleImport) => ` ${styleImport}`),
|
|
472
|
-
].join('\n'),
|
|
473
|
-
)
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
steps.push(
|
|
295
|
+
function printNextSteps({ envChanged, dependencyChanged }) {
|
|
296
|
+
const steps = [
|
|
477
297
|
[
|
|
478
298
|
'Render <TinyRobotChat /> near your main application component.',
|
|
479
299
|
'',
|
|
@@ -488,12 +308,12 @@ function printNextSteps({ needsManualStyleImport, envChanged, dependencyChanged
|
|
|
488
308
|
' <TinyRobotChat />',
|
|
489
309
|
' </template>',
|
|
490
310
|
].join('\n'),
|
|
491
|
-
|
|
311
|
+
]
|
|
492
312
|
|
|
493
313
|
if (envChanged) {
|
|
494
314
|
steps.push(
|
|
495
315
|
[
|
|
496
|
-
'
|
|
316
|
+
'Copy the required variables from .env.example to your private environment file and configure a provider key.',
|
|
497
317
|
'',
|
|
498
318
|
'Example:',
|
|
499
319
|
'',
|
|
@@ -506,10 +326,6 @@ function printNextSteps({ needsManualStyleImport, envChanged, dependencyChanged
|
|
|
506
326
|
steps.push(['Install or update project dependencies.', '', 'Example:', '', ' pnpm install'].join('\n'))
|
|
507
327
|
}
|
|
508
328
|
|
|
509
|
-
if (steps.length === 0) {
|
|
510
|
-
return
|
|
511
|
-
}
|
|
512
|
-
|
|
513
329
|
console.log('\nNext Steps\n')
|
|
514
330
|
|
|
515
331
|
for (const [index, step] of steps.entries()) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opentiny/tiny-robot-cli",
|
|
3
|
-
"version": "0.5.2-alpha.
|
|
3
|
+
"version": "0.5.2-alpha.12",
|
|
4
4
|
"description": "CLI to scaffold TinyRobot product projects",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
"README.md"
|
|
20
20
|
],
|
|
21
21
|
"scripts": {
|
|
22
|
-
"start": "node ./bin/cli.js"
|
|
22
|
+
"start": "node ./bin/cli.js",
|
|
23
|
+
"test": "node --test ./test/*.test.js"
|
|
23
24
|
},
|
|
24
25
|
"license": "MIT",
|
|
25
26
|
"engines": {
|
|
@@ -32,5 +33,5 @@
|
|
|
32
33
|
"semver": "^7.8.1",
|
|
33
34
|
"yaml": "^2.9.0"
|
|
34
35
|
},
|
|
35
|
-
"gitHead": "
|
|
36
|
+
"gitHead": "dc033027731d2d5b863529cc65b5314a1daa33ff"
|
|
36
37
|
}
|
package/templates/chat/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# chat-add
|
|
2
2
|
|
|
3
|
-
`chat-add`
|
|
3
|
+
`chat-add` 是一个可嵌入现有 Vue 应用的 Chat 扩展模板,提供通用工作助手场景的最小工程结构。
|
|
4
4
|
|
|
5
5
|
## 启动
|
|
6
6
|
|
|
@@ -17,19 +17,21 @@ pnpm --filter chat-add build
|
|
|
17
17
|
|
|
18
18
|
## 接入真实模型
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
将生成的 `.env.example` 复制为 `.env.local`,按需配置 `VITE_ALIYUN_DASHSCOPE_KEY` 和 `VITE_DEEPSEEK_API_KEY`,重启 Vite 后即可使用模型选择器中的真实模型。
|
|
21
|
+
|
|
22
|
+
Qwen 和 DeepSeek 默认使用 `@opentiny/tiny-robot-chat` 内置的官方接口地址。仅在使用代理、私有网关或其他地域接口时,才需要在 `.env.local` 中额外配置 `VITE_QWEN_API_URL` 或 `VITE_DEEPSEEK_API_URL`。配置读取逻辑位于 `src/tiny-robot-chat/config/chat-runtime.ts`。
|
|
21
23
|
|
|
22
24
|
## 目录职责
|
|
23
25
|
|
|
24
|
-
- `src/
|
|
25
|
-
- `src/config/chat-
|
|
26
|
-
- `src/
|
|
27
|
-
- `src/
|
|
28
|
-
- `src/
|
|
26
|
+
- `src/TinyRobotChat.vue`: 悬浮入口、Chat 主体以及组件作用域样式。
|
|
27
|
+
- `src/tiny-robot-chat/config/chat-ui.ts`: Chat UI、提示词和模板配置。
|
|
28
|
+
- `src/tiny-robot-chat/config/chat-runtime.ts`: 模型配置和留给后续集成的空 MCP Server 列表。
|
|
29
|
+
- `src/tiny-robot-chat/components`: 窗口头部和输入区工具。
|
|
30
|
+
- `src/tiny-robot-chat/composables/useWindow.ts`: floating、fullscreen 窗口状态。
|
|
29
31
|
|
|
30
32
|
## 替换数据
|
|
31
33
|
|
|
32
|
-
直接修改 `src/config/chat-ui.ts` 和 `src/config/chat-runtime.ts` 中导出的数据;组件通过 props 使用这些配置。
|
|
34
|
+
直接修改 `src/tiny-robot-chat/config/chat-ui.ts` 和 `src/tiny-robot-chat/config/chat-runtime.ts` 中导出的数据;组件通过 props 使用这些配置。
|
|
33
35
|
|
|
34
36
|
## 切换窗口模式
|
|
35
37
|
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import type { HistoryMenuItem, PromptProps, TemplateItem } from '@opentiny/tiny-robot'
|
|
3
3
|
import { TrSender, TrThemeProvider as TrTheme } from '@opentiny/tiny-robot'
|
|
4
|
+
import '@opentiny/tiny-robot/dist/style.css'
|
|
4
5
|
import { TrChat, useChatHistoryItems, useLocalChatRuntime, type ChatHistoryItem } from '@opentiny/tiny-robot-chat'
|
|
6
|
+
import '@opentiny/tiny-robot-chat/dist/style.css'
|
|
5
7
|
import { IconAi } from '@opentiny/tiny-robot-svgs'
|
|
6
8
|
import { computed, reactive, ref } from 'vue'
|
|
7
|
-
import ComposerTools from './components/ComposerTools.vue'
|
|
8
|
-
import WindowHeader from './components/WindowHeader.vue'
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import { useWindow } from './composables/useWindow'
|
|
9
|
+
import ComposerTools from './tiny-robot-chat/components/ComposerTools.vue'
|
|
10
|
+
import WindowHeader from './tiny-robot-chat/components/WindowHeader.vue'
|
|
11
|
+
import { createChatUi, templateCategories } from './tiny-robot-chat/config/chat-ui'
|
|
12
|
+
import { mcpServers, modelProviders } from './tiny-robot-chat/config/chat-runtime'
|
|
13
|
+
import { useWindow } from './tiny-robot-chat/composables/useWindow'
|
|
12
14
|
|
|
13
15
|
const runtime = useLocalChatRuntime({ modelProviders, mcpServers })
|
|
14
16
|
const window = reactive(useWindow())
|
|
@@ -45,52 +47,130 @@ function handleHistoryAction(action: HistoryMenuItem, item: ChatHistoryItem): vo
|
|
|
45
47
|
|
|
46
48
|
<template>
|
|
47
49
|
<TrTheme>
|
|
48
|
-
<
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
>
|
|
69
|
-
<
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
<template
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
:template-categories="templateCategories"
|
|
88
|
-
:mcp-examples="mcpExamples"
|
|
89
|
-
:menus="composerMenus"
|
|
90
|
-
:submit="submit"
|
|
91
|
-
/>
|
|
92
|
-
</template>
|
|
93
|
-
</TrChat>
|
|
94
|
-
</main>
|
|
50
|
+
<button
|
|
51
|
+
v-if="!window.show"
|
|
52
|
+
class="chat-add-launcher"
|
|
53
|
+
type="button"
|
|
54
|
+
title="打开 TinyRobot"
|
|
55
|
+
aria-label="打开 TinyRobot"
|
|
56
|
+
@click="window.open"
|
|
57
|
+
>
|
|
58
|
+
<IconAi style="font-size: 32px" />
|
|
59
|
+
</button>
|
|
60
|
+
<TrChat
|
|
61
|
+
v-if="window.show"
|
|
62
|
+
ref="chatRef"
|
|
63
|
+
:class="['chat-add-window', `chat-add-window--${window.displayMode}`]"
|
|
64
|
+
:style="window.layoutStyle"
|
|
65
|
+
:runtime="runtime"
|
|
66
|
+
:ui="chatUi"
|
|
67
|
+
v-model:floating-state="window.floatingState"
|
|
68
|
+
@prompt-click="handlePromptClick"
|
|
69
|
+
>
|
|
70
|
+
<template #layout-header>
|
|
71
|
+
<WindowHeader
|
|
72
|
+
:display-mode="window.displayMode"
|
|
73
|
+
:show-history="showHistory"
|
|
74
|
+
:history-data="historyData"
|
|
75
|
+
:active-conversation-id="activeConversationId"
|
|
76
|
+
@new-session="handleNewSession"
|
|
77
|
+
@select-history="handleHistorySelect"
|
|
78
|
+
@rename-history="handleHistoryRename"
|
|
79
|
+
@history-action="handleHistoryAction"
|
|
80
|
+
@update:show-history="showHistory = $event"
|
|
81
|
+
@change-mode="window.setDisplayMode"
|
|
82
|
+
@close="window.close"
|
|
83
|
+
/>
|
|
84
|
+
</template>
|
|
85
|
+
<template #composer-before>
|
|
86
|
+
<ComposerTools v-model:current-template="currentTemplate" :template-categories="templateCategories" />
|
|
87
|
+
</template>
|
|
88
|
+
</TrChat>
|
|
95
89
|
</TrTheme>
|
|
96
90
|
</template>
|
|
91
|
+
|
|
92
|
+
<style scoped>
|
|
93
|
+
.chat-add-launcher {
|
|
94
|
+
position: fixed;
|
|
95
|
+
right: 24px;
|
|
96
|
+
bottom: 24px;
|
|
97
|
+
z-index: var(--tr-z-index-popover, 1000);
|
|
98
|
+
display: inline-flex;
|
|
99
|
+
align-items: center;
|
|
100
|
+
justify-content: center;
|
|
101
|
+
width: 56px;
|
|
102
|
+
height: 56px;
|
|
103
|
+
padding: 0;
|
|
104
|
+
border: 0;
|
|
105
|
+
border-radius: 50%;
|
|
106
|
+
background: var(--tr-container-bg-default);
|
|
107
|
+
color: var(--tr-color-primary);
|
|
108
|
+
box-shadow: 0 8px 24px rgb(0 0 0 / 20%);
|
|
109
|
+
cursor: pointer;
|
|
110
|
+
}
|
|
111
|
+
.chat-add-launcher:hover {
|
|
112
|
+
background: var(--tr-color-primary-light);
|
|
113
|
+
}
|
|
114
|
+
.chat-add-launcher:focus-visible {
|
|
115
|
+
outline: 2px solid var(--tr-color-primary);
|
|
116
|
+
outline-offset: 3px;
|
|
117
|
+
}
|
|
118
|
+
@media (max-width: 519px) {
|
|
119
|
+
.chat-add-launcher {
|
|
120
|
+
right: 16px;
|
|
121
|
+
bottom: 16px;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
</style>
|
|
125
|
+
|
|
126
|
+
<style>
|
|
127
|
+
.chat-add-window {
|
|
128
|
+
--chat-prompt-min-width: 280px;
|
|
129
|
+
--tr-layout-bg-default: #f5f5f7;
|
|
130
|
+
--tr-chat-ui-header-bg: #f5f5f7;
|
|
131
|
+
--tr-chat-ui-main-bg: #f5f5f7;
|
|
132
|
+
--tr-chat-ui-footer-bg: #f5f5f7;
|
|
133
|
+
min-width: 0;
|
|
134
|
+
}
|
|
135
|
+
.chat-add-window .tr-welcome {
|
|
136
|
+
box-sizing: border-box;
|
|
137
|
+
width: 100%;
|
|
138
|
+
padding: 24px 24px 0;
|
|
139
|
+
}
|
|
140
|
+
.chat-add-window .tr-welcome__title-wrapper {
|
|
141
|
+
display: flex;
|
|
142
|
+
align-items: center;
|
|
143
|
+
justify-content: center;
|
|
144
|
+
}
|
|
145
|
+
.chat-add-window .tr-prompts {
|
|
146
|
+
--tr-prompts-gap: 16px;
|
|
147
|
+
box-sizing: border-box;
|
|
148
|
+
width: 100%;
|
|
149
|
+
padding: 16px 24px;
|
|
150
|
+
}
|
|
151
|
+
.chat-add-window .tr-prompts .tr-prompts__list-container.wrap {
|
|
152
|
+
display: grid;
|
|
153
|
+
grid-template-columns: repeat(2, minmax(var(--chat-prompt-min-width), 1fr));
|
|
154
|
+
}
|
|
155
|
+
.chat-add-window .tr-prompts__list-container > .prompt-item {
|
|
156
|
+
box-sizing: border-box;
|
|
157
|
+
width: 100%;
|
|
158
|
+
min-width: 0;
|
|
159
|
+
}
|
|
160
|
+
@container (max-width: 623px) {
|
|
161
|
+
.chat-add-window .tr-prompts .tr-prompts__list-container.wrap {
|
|
162
|
+
grid-template-columns: minmax(0, 1fr);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
@container (max-width: 519px) {
|
|
166
|
+
.chat-add-window .tr-welcome {
|
|
167
|
+
padding-inline: 12px;
|
|
168
|
+
}
|
|
169
|
+
.chat-add-window .tr-prompts {
|
|
170
|
+
padding-inline: 12px;
|
|
171
|
+
}
|
|
172
|
+
.chat-add-window .tr-bubble__avatar {
|
|
173
|
+
display: none;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
</style>
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { TemplateItem } from '@opentiny/tiny-robot'
|
|
3
|
+
import { TrDropdownMenu, TrSuggestionPillButton, TrSuggestionPills } from '@opentiny/tiny-robot'
|
|
4
|
+
import type { TemplateCategory, TemplateSuggestionItem } from '../config/chat-ui'
|
|
5
|
+
import { computed } from 'vue'
|
|
6
|
+
|
|
7
|
+
const props = defineProps<{
|
|
8
|
+
templateCategories: readonly TemplateCategory[]
|
|
9
|
+
}>()
|
|
10
|
+
|
|
11
|
+
const currentTemplate = defineModel<TemplateItem[]>('currentTemplate', { required: true })
|
|
12
|
+
const templateMenus = computed(() =>
|
|
13
|
+
props.templateCategories.map((category) => ({
|
|
14
|
+
...category,
|
|
15
|
+
items: category.items.map((item) => ({ id: item.id, text: item.text, template: item })),
|
|
16
|
+
})),
|
|
17
|
+
)
|
|
18
|
+
const overlayTarget = '.chat-add-window'
|
|
19
|
+
|
|
20
|
+
function fillTemplate(template: TemplateSuggestionItem): void {
|
|
21
|
+
currentTemplate.value = template.template.map((item) => ({ ...item }))
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function handleTemplateSelect(item: unknown): void {
|
|
25
|
+
fillTemplate((item as { template: TemplateSuggestionItem }).template)
|
|
26
|
+
}
|
|
27
|
+
</script>
|
|
28
|
+
|
|
29
|
+
<template>
|
|
30
|
+
<div class="chat-add-tools">
|
|
31
|
+
<TrSuggestionPills class="chat-add-tools__pills">
|
|
32
|
+
<TrDropdownMenu
|
|
33
|
+
v-for="category in templateMenus"
|
|
34
|
+
:key="category.id"
|
|
35
|
+
:append-to="overlayTarget"
|
|
36
|
+
:items="category.items as never"
|
|
37
|
+
trigger="click"
|
|
38
|
+
@item-click="handleTemplateSelect"
|
|
39
|
+
>
|
|
40
|
+
<template #trigger
|
|
41
|
+
><TrSuggestionPillButton>{{ category.title }}</TrSuggestionPillButton></template
|
|
42
|
+
>
|
|
43
|
+
</TrDropdownMenu>
|
|
44
|
+
</TrSuggestionPills>
|
|
45
|
+
</div>
|
|
46
|
+
</template>
|
|
47
|
+
|
|
48
|
+
<style scoped>
|
|
49
|
+
.chat-add-tools {
|
|
50
|
+
display: flex;
|
|
51
|
+
box-sizing: border-box;
|
|
52
|
+
width: 100%;
|
|
53
|
+
min-width: 0;
|
|
54
|
+
flex-shrink: 0;
|
|
55
|
+
align-items: center;
|
|
56
|
+
gap: 8px;
|
|
57
|
+
padding: 8px 12px;
|
|
58
|
+
}
|
|
59
|
+
.chat-add-tools__pills {
|
|
60
|
+
min-width: 0;
|
|
61
|
+
flex: 1;
|
|
62
|
+
}
|
|
63
|
+
.chat-add-tools__pills :deep(.tr-suggestion-pills__container) {
|
|
64
|
+
min-width: 0;
|
|
65
|
+
mask: linear-gradient(to right, rgb(0 0 0 / 100%) 80%, rgb(0 0 0 / 0%) 100%);
|
|
66
|
+
}
|
|
67
|
+
@container (max-width: 519px) {
|
|
68
|
+
.chat-add-tools {
|
|
69
|
+
padding: 8px;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
</style>
|
|
@@ -4,24 +4,34 @@ import { computed, shallowRef, watch } from 'vue'
|
|
|
4
4
|
|
|
5
5
|
export type DisplayMode = 'floating' | 'fullscreen'
|
|
6
6
|
|
|
7
|
+
const FLOATING_GAP = 24
|
|
8
|
+
const FLOATING_MIN_WIDTH = 360
|
|
9
|
+
const FLOATING_MIN_HEIGHT = 480
|
|
10
|
+
|
|
7
11
|
export function useWindow() {
|
|
8
12
|
const show = shallowRef(false)
|
|
9
13
|
const displayMode = shallowRef<DisplayMode>('floating')
|
|
14
|
+
const { width, height } = useWindowSize()
|
|
10
15
|
const floatingState = shallowRef<LayoutFloatingState>({
|
|
11
|
-
placement: '
|
|
12
|
-
offsetX:
|
|
13
|
-
offsetY:
|
|
14
|
-
width:
|
|
15
|
-
height:
|
|
16
|
+
placement: 'top-right',
|
|
17
|
+
offsetX: FLOATING_GAP,
|
|
18
|
+
offsetY: FLOATING_GAP,
|
|
19
|
+
width: 500,
|
|
20
|
+
height: Math.max(1, height.value - FLOATING_GAP * 2),
|
|
16
21
|
})
|
|
17
22
|
const restoreFloatingState = shallowRef<LayoutFloatingState | null>(null)
|
|
18
|
-
const { width, height } = useWindowSize()
|
|
19
23
|
|
|
20
24
|
const floatingOptions = computed<LayoutFloatingOptions>(() => ({
|
|
21
25
|
draggable: displayMode.value === 'floating',
|
|
22
26
|
resizable: displayMode.value === 'floating',
|
|
23
|
-
minWidth:
|
|
24
|
-
|
|
27
|
+
minWidth:
|
|
28
|
+
displayMode.value === 'floating'
|
|
29
|
+
? Math.min(FLOATING_MIN_WIDTH, Math.max(1, width.value - FLOATING_GAP * 2))
|
|
30
|
+
: undefined,
|
|
31
|
+
minHeight:
|
|
32
|
+
displayMode.value === 'floating'
|
|
33
|
+
? Math.min(FLOATING_MIN_HEIGHT, Math.max(1, height.value - FLOATING_GAP * 2))
|
|
34
|
+
: undefined,
|
|
25
35
|
}))
|
|
26
36
|
|
|
27
37
|
const layoutStyle = computed(() => ({
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ChatMcpServers, ChatProviderConfig } from '@opentiny/tiny-robot-chat'
|
|
2
|
+
|
|
3
|
+
export const mcpServers: ChatMcpServers = []
|
|
4
|
+
|
|
5
|
+
export const modelProviders: ChatProviderConfig[] = [
|
|
6
|
+
{
|
|
7
|
+
type: 'qwen',
|
|
8
|
+
label: 'DashScope',
|
|
9
|
+
apiUrl: import.meta.env.VITE_QWEN_API_URL?.trim() || undefined,
|
|
10
|
+
apiKey: import.meta.env.VITE_ALIYUN_DASHSCOPE_KEY?.trim(),
|
|
11
|
+
models: [
|
|
12
|
+
{ id: 'qwen3.7-flash', label: 'Qwen3.7 Flash', capabilities: { thinking: true, search: true } },
|
|
13
|
+
{ id: 'qwen3.7-plus', label: 'Qwen3.7 Plus', capabilities: { thinking: true, search: true } },
|
|
14
|
+
{ id: 'qwen3.7-max', label: 'Qwen3.7 Max', capabilities: { thinking: true, search: true } },
|
|
15
|
+
],
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
type: 'deepseek',
|
|
19
|
+
apiUrl: import.meta.env.VITE_DEEPSEEK_API_URL?.trim() || undefined,
|
|
20
|
+
apiKey: import.meta.env.VITE_DEEPSEEK_API_KEY?.trim(),
|
|
21
|
+
models: [
|
|
22
|
+
{ id: 'deepseek-v4-flash', label: 'DeepSeek V4 Flash', capabilities: { thinking: true } },
|
|
23
|
+
{ id: 'deepseek-v4-pro', label: 'DeepSeek V4 Pro', capabilities: { thinking: true } },
|
|
24
|
+
],
|
|
25
|
+
},
|
|
26
|
+
]
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { LayoutFloatingOptions } from '@opentiny/tiny-robot'
|
|
2
2
|
import type { ChatSenderOptions, ChatUIOptions } from '@opentiny/tiny-robot-chat'
|
|
3
3
|
import type { TemplateItem } from '@opentiny/tiny-robot'
|
|
4
|
-
import { IconAi,
|
|
4
|
+
import { IconAi, IconUser } from '@opentiny/tiny-robot-svgs'
|
|
5
5
|
import { h, type CSSProperties } from 'vue'
|
|
6
6
|
|
|
7
7
|
export interface ChatUiConfigOptions {
|
|
@@ -9,24 +9,6 @@ export interface ChatUiConfigOptions {
|
|
|
9
9
|
templateExtensions: NonNullable<ChatSenderOptions['extensions']>
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
export interface ChatMenuItem {
|
|
13
|
-
id: string
|
|
14
|
-
title: string
|
|
15
|
-
icon: unknown
|
|
16
|
-
action: 'mcp' | 'template' | 'history' | 'new-session' | 'mode'
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export const composerMenus: ChatMenuItem[] = [
|
|
20
|
-
{ id: 'quick-query', title: '常用查询', icon: IconSparkles, action: 'mcp' },
|
|
21
|
-
{ id: 'templates', title: '模板', icon: IconEdit, action: 'template' },
|
|
22
|
-
]
|
|
23
|
-
|
|
24
|
-
export const windowMenus: ChatMenuItem[] = [
|
|
25
|
-
{ id: 'new-session', title: '新会话', icon: IconNewSession, action: 'new-session' },
|
|
26
|
-
{ id: 'history', title: '历史会话', icon: IconHistory, action: 'history' },
|
|
27
|
-
{ id: 'mode', title: '窗口模式', icon: IconSparkles, action: 'mode' },
|
|
28
|
-
]
|
|
29
|
-
|
|
30
12
|
export interface PromptItemData {
|
|
31
13
|
label: string
|
|
32
14
|
description: string
|
|
@@ -52,9 +34,9 @@ export const PROMPT_ITEMS_DATA: PromptItemData[] = [
|
|
|
52
34
|
emoji: '✨',
|
|
53
35
|
},
|
|
54
36
|
{
|
|
55
|
-
label: '
|
|
56
|
-
description: '
|
|
57
|
-
emoji: '
|
|
37
|
+
label: '工作效率场景',
|
|
38
|
+
description: '需要我帮你总结内容、拆解任务,还是整理一份行动清单?',
|
|
39
|
+
emoji: '⚡',
|
|
58
40
|
},
|
|
59
41
|
]
|
|
60
42
|
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
import type { SuggestionData, SuggestionItem, TemplateItem } from '@opentiny/tiny-robot'
|
|
3
|
-
import { TrDropdownMenu, TrSuggestionPillButton, TrSuggestionPills, TrSuggestionPopover } from '@opentiny/tiny-robot'
|
|
4
|
-
import type { ChatSenderSlotProps } from '@opentiny/tiny-robot-chat'
|
|
5
|
-
import { IconSparkles } from '@opentiny/tiny-robot-svgs'
|
|
6
|
-
import type { ChatMenuItem, TemplateCategory, TemplateSuggestionItem } from '../config/chat-ui'
|
|
7
|
-
import type { McpExample } from '../config/chat-runtime'
|
|
8
|
-
import { computed } from 'vue'
|
|
9
|
-
|
|
10
|
-
const props = defineProps<{
|
|
11
|
-
templateCategories: readonly TemplateCategory[]
|
|
12
|
-
mcpExamples: readonly McpExample[]
|
|
13
|
-
menus: readonly ChatMenuItem[]
|
|
14
|
-
submit: ChatSenderSlotProps['submit']
|
|
15
|
-
}>()
|
|
16
|
-
|
|
17
|
-
const currentTemplate = defineModel<TemplateItem[]>('currentTemplate', { required: true })
|
|
18
|
-
const suggestionData: SuggestionData = props.mcpExamples.map((item) => ({ id: item.id, text: item.request }))
|
|
19
|
-
const mcpMenuItems = props.mcpExamples.map((item) => ({ id: item.id, text: item.request }))
|
|
20
|
-
const mcpMenu = computed(() => props.menus.find((menu) => menu.action === 'mcp'))
|
|
21
|
-
const templateMenus = computed(() =>
|
|
22
|
-
props.templateCategories.map((category) => ({
|
|
23
|
-
...category,
|
|
24
|
-
items: category.items.map((item) => ({ id: item.id, text: item.text, template: item })),
|
|
25
|
-
})),
|
|
26
|
-
)
|
|
27
|
-
const overlayTarget = '.chat-add-window'
|
|
28
|
-
|
|
29
|
-
function fillTemplate(template: TemplateSuggestionItem): void {
|
|
30
|
-
currentTemplate.value = template.template.map((item) => ({ ...item }))
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function submitSuggestion(item: SuggestionItem): void {
|
|
34
|
-
props.submit({ text: item.text })
|
|
35
|
-
}
|
|
36
|
-
</script>
|
|
37
|
-
|
|
38
|
-
<template>
|
|
39
|
-
<div class="chat-add-tools">
|
|
40
|
-
<TrSuggestionPopover :append-to="overlayTarget" :data="suggestionData" @item-click="submitSuggestion">
|
|
41
|
-
<template #trigger>
|
|
42
|
-
<TrSuggestionPillButton title="常用查询"
|
|
43
|
-
><IconSparkles style="font-size: 16px; color: #1476ff"
|
|
44
|
-
/></TrSuggestionPillButton>
|
|
45
|
-
</template>
|
|
46
|
-
</TrSuggestionPopover>
|
|
47
|
-
<TrSuggestionPills class="chat-add-tools__pills">
|
|
48
|
-
<TrDropdownMenu
|
|
49
|
-
v-if="mcpMenu"
|
|
50
|
-
:append-to="overlayTarget"
|
|
51
|
-
:items="mcpMenuItems"
|
|
52
|
-
trigger="click"
|
|
53
|
-
@item-click="(item) => submitSuggestion(item as SuggestionItem)"
|
|
54
|
-
>
|
|
55
|
-
<template #trigger
|
|
56
|
-
><TrSuggestionPillButton>{{ mcpMenu.title }}</TrSuggestionPillButton></template
|
|
57
|
-
>
|
|
58
|
-
</TrDropdownMenu>
|
|
59
|
-
<TrDropdownMenu
|
|
60
|
-
v-for="category in templateMenus"
|
|
61
|
-
:key="category.id"
|
|
62
|
-
:append-to="overlayTarget"
|
|
63
|
-
:items="category.items as never"
|
|
64
|
-
trigger="click"
|
|
65
|
-
@item-click="(item) => fillTemplate((item as unknown as { template: TemplateSuggestionItem }).template)"
|
|
66
|
-
>
|
|
67
|
-
<template #trigger
|
|
68
|
-
><TrSuggestionPillButton>{{ category.title }}</TrSuggestionPillButton></template
|
|
69
|
-
>
|
|
70
|
-
</TrDropdownMenu>
|
|
71
|
-
</TrSuggestionPills>
|
|
72
|
-
</div>
|
|
73
|
-
</template>
|
|
74
|
-
|
|
75
|
-
<style scoped>
|
|
76
|
-
.chat-add-tools {
|
|
77
|
-
display: flex;
|
|
78
|
-
box-sizing: border-box;
|
|
79
|
-
width: 100%;
|
|
80
|
-
min-width: 0;
|
|
81
|
-
flex-shrink: 0;
|
|
82
|
-
align-items: center;
|
|
83
|
-
gap: 8px;
|
|
84
|
-
padding: 8px 12px;
|
|
85
|
-
}
|
|
86
|
-
.chat-add-tools__pills {
|
|
87
|
-
min-width: 0;
|
|
88
|
-
flex: 1;
|
|
89
|
-
}
|
|
90
|
-
.chat-add-tools__pills :deep(.tr-suggestion-pills__container) {
|
|
91
|
-
min-width: 0;
|
|
92
|
-
mask: linear-gradient(to right, rgb(0 0 0 / 100%) 80%, rgb(0 0 0 / 0%) 100%);
|
|
93
|
-
}
|
|
94
|
-
@container (max-width: 519px) {
|
|
95
|
-
.chat-add-tools {
|
|
96
|
-
padding: 8px;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
</style>
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import type { ChatMcpServers, ChatProviderConfig } from '@opentiny/tiny-robot-chat'
|
|
2
|
-
|
|
3
|
-
export interface McpExample {
|
|
4
|
-
id: string
|
|
5
|
-
title: string
|
|
6
|
-
request: string
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export const mcpExamples: McpExample[] = [
|
|
10
|
-
{ id: 'weather', title: '查询北京天气', request: '查询北京今天的天气,并给出出行建议' },
|
|
11
|
-
{ id: 'coffee', title: '查询附近咖啡店', request: '查询我附近的咖啡店,并按距离排序' },
|
|
12
|
-
{ id: 'exchange-rate', title: '获取当前汇率', request: '查询人民币兑美元的当前汇率' },
|
|
13
|
-
]
|
|
14
|
-
|
|
15
|
-
export const mcpServers: ChatMcpServers = [
|
|
16
|
-
{
|
|
17
|
-
id: 'amap-maps',
|
|
18
|
-
name: '高德地图',
|
|
19
|
-
description: '覆盖地图、导航、地理编码、天气、路径规划、距离测量、关键词搜索和周边搜索等地理信息服务。',
|
|
20
|
-
baseUrl: import.meta.env.VITE_AMAP_MCP_URL?.trim() || 'https://dashscope.aliyuncs.com/api/v1/mcps/amap-maps/mcp',
|
|
21
|
-
icon: 'https://img.alicdn.com/imgextra/i4/O1CN01iPPabT1EGRN6uatHP_!!6000000000324-0-tps-512-512.jpg',
|
|
22
|
-
headers: import.meta.env.VITE_ALIYUN_DASHSCOPE_KEY?.trim()
|
|
23
|
-
? { Authorization: `Bearer ${import.meta.env.VITE_ALIYUN_DASHSCOPE_KEY.trim()}` }
|
|
24
|
-
: undefined,
|
|
25
|
-
},
|
|
26
|
-
{
|
|
27
|
-
id: 'model-context-protocol-mcp',
|
|
28
|
-
name: 'Model Context Protocol MCP',
|
|
29
|
-
description: 'MCP Server',
|
|
30
|
-
baseUrl: '/modelcontextprotocol-mcp',
|
|
31
|
-
icon: '/modelcontextprotocol.png',
|
|
32
|
-
installed: true,
|
|
33
|
-
},
|
|
34
|
-
]
|
|
35
|
-
|
|
36
|
-
export const modelProviders: ChatProviderConfig[] = [
|
|
37
|
-
{
|
|
38
|
-
type: 'qwen',
|
|
39
|
-
label: 'DashScope',
|
|
40
|
-
apiUrl: import.meta.env.VITE_QWEN_API_URL?.trim() || undefined,
|
|
41
|
-
apiKey: import.meta.env.VITE_ALIYUN_DASHSCOPE_KEY?.trim(),
|
|
42
|
-
models: [
|
|
43
|
-
{ id: 'qwen3.7-flash', label: 'Qwen3.7 Flash', capabilities: { thinking: true, search: true } },
|
|
44
|
-
{ id: 'qwen3.7-plus', label: 'Qwen3.7 Plus', capabilities: { thinking: true, search: true } },
|
|
45
|
-
{ id: 'qwen3.7-max', label: 'Qwen3.7 Max', capabilities: { thinking: true, search: true } },
|
|
46
|
-
],
|
|
47
|
-
},
|
|
48
|
-
{
|
|
49
|
-
type: 'deepseek',
|
|
50
|
-
apiUrl: import.meta.env.VITE_DEEPSEEK_API_URL?.trim() || undefined,
|
|
51
|
-
apiKey: import.meta.env.VITE_DEEPSEEK_API_KEY?.trim(),
|
|
52
|
-
models: [
|
|
53
|
-
{ id: 'deepseek-v4-flash', label: 'DeepSeek V4 Flash', capabilities: { thinking: true } },
|
|
54
|
-
{ id: 'deepseek-v4-pro', label: 'DeepSeek V4 Pro', capabilities: { thinking: true } },
|
|
55
|
-
],
|
|
56
|
-
},
|
|
57
|
-
]
|
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
:root {
|
|
2
|
-
font-family:
|
|
3
|
-
Inter,
|
|
4
|
-
-apple-system,
|
|
5
|
-
BlinkMacSystemFont,
|
|
6
|
-
'Segoe UI',
|
|
7
|
-
sans-serif;
|
|
8
|
-
color: #1f2329;
|
|
9
|
-
}
|
|
10
|
-
* {
|
|
11
|
-
box-sizing: border-box;
|
|
12
|
-
}
|
|
13
|
-
html,
|
|
14
|
-
body,
|
|
15
|
-
#app {
|
|
16
|
-
width: 100%;
|
|
17
|
-
height: 100%;
|
|
18
|
-
min-height: 0;
|
|
19
|
-
margin: 0;
|
|
20
|
-
}
|
|
21
|
-
body {
|
|
22
|
-
overflow: hidden;
|
|
23
|
-
}
|
|
24
|
-
.chat-add-app {
|
|
25
|
-
position: relative;
|
|
26
|
-
container-type: inline-size;
|
|
27
|
-
display: flex;
|
|
28
|
-
width: 100%;
|
|
29
|
-
height: 100%;
|
|
30
|
-
min-width: 0;
|
|
31
|
-
min-height: 0;
|
|
32
|
-
}
|
|
33
|
-
.chat-add-launcher {
|
|
34
|
-
position: fixed;
|
|
35
|
-
right: 24px;
|
|
36
|
-
bottom: 24px;
|
|
37
|
-
z-index: var(--tr-z-index-popover, 1000);
|
|
38
|
-
display: inline-flex;
|
|
39
|
-
align-items: center;
|
|
40
|
-
justify-content: center;
|
|
41
|
-
width: 56px;
|
|
42
|
-
height: 56px;
|
|
43
|
-
padding: 0;
|
|
44
|
-
border: 0;
|
|
45
|
-
border-radius: 50%;
|
|
46
|
-
background: var(--tr-color-primary);
|
|
47
|
-
color: #fff;
|
|
48
|
-
box-shadow: 0 8px 24px rgb(0 0 0 / 20%);
|
|
49
|
-
cursor: pointer;
|
|
50
|
-
}
|
|
51
|
-
.chat-add-launcher:hover {
|
|
52
|
-
background: var(--tr-color-primary-hover);
|
|
53
|
-
}
|
|
54
|
-
.chat-add-launcher:focus-visible {
|
|
55
|
-
outline: 2px solid var(--tr-color-primary);
|
|
56
|
-
outline-offset: 3px;
|
|
57
|
-
}
|
|
58
|
-
.chat-add-window {
|
|
59
|
-
container-type: inline-size;
|
|
60
|
-
--tr-layout-bg-default: #f5f5f7;
|
|
61
|
-
--tr-chat-ui-header-bg: #f5f5f7;
|
|
62
|
-
--tr-chat-ui-main-bg: #f5f5f7;
|
|
63
|
-
--tr-chat-ui-footer-bg: #f5f5f7;
|
|
64
|
-
--tr-suggestion-popover-width: min(440px, calc(100cqw - 16px));
|
|
65
|
-
width: 100%;
|
|
66
|
-
min-width: 0;
|
|
67
|
-
min-height: 0;
|
|
68
|
-
}
|
|
69
|
-
.chat-add-window .tr-welcome {
|
|
70
|
-
box-sizing: border-box;
|
|
71
|
-
width: 100%;
|
|
72
|
-
padding: 24px 24px 0;
|
|
73
|
-
}
|
|
74
|
-
.chat-add-window .tr-welcome__title-wrapper {
|
|
75
|
-
display: flex;
|
|
76
|
-
align-items: center;
|
|
77
|
-
justify-content: center;
|
|
78
|
-
}
|
|
79
|
-
.chat-add-window .tr-prompts {
|
|
80
|
-
--tr-prompts-gap: 16px;
|
|
81
|
-
box-sizing: border-box;
|
|
82
|
-
width: 100%;
|
|
83
|
-
padding: 16px 24px;
|
|
84
|
-
}
|
|
85
|
-
.chat-add-window .prompt-item {
|
|
86
|
-
flex: 0 0 calc((100% - 16px) / 2);
|
|
87
|
-
box-sizing: border-box;
|
|
88
|
-
min-width: 0;
|
|
89
|
-
}
|
|
90
|
-
.chat-add-window--floating {
|
|
91
|
-
border-radius: var(--tr-layout-floating-radius);
|
|
92
|
-
box-shadow: var(--tr-layout-floating-shadow);
|
|
93
|
-
overflow: hidden;
|
|
94
|
-
}
|
|
95
|
-
.chat-add-window--fullscreen {
|
|
96
|
-
border-radius: 0;
|
|
97
|
-
box-shadow: none;
|
|
98
|
-
}
|
|
99
|
-
@container (max-width: 519px) {
|
|
100
|
-
.chat-add-launcher {
|
|
101
|
-
right: 16px;
|
|
102
|
-
bottom: 16px;
|
|
103
|
-
}
|
|
104
|
-
.chat-add-window .prompt-item {
|
|
105
|
-
flex-basis: 100%;
|
|
106
|
-
}
|
|
107
|
-
.chat-add-window .tr-welcome {
|
|
108
|
-
padding-inline: 12px;
|
|
109
|
-
}
|
|
110
|
-
.chat-add-window .tr-prompts {
|
|
111
|
-
padding-inline: 12px;
|
|
112
|
-
}
|
|
113
|
-
.chat-add-window .tr-bubble__avatar {
|
|
114
|
-
display: none;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
File without changes
|