@opentiny/tiny-robot-cli 1.0.0-beta.1 → 1.0.0-rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -8
- package/bin/commands/add.js +160 -376
- package/bin/utils.js +1 -1
- package/package.json +2 -3
- package/templates/basic/README.md +1 -1
- package/templates/basic/src/App.vue +2 -2
- package/templates/chat/.env.example +2 -1
- package/templates/chat/README.md +11 -2
- package/templates/chat/src/TinyRobotChat.vue +5 -2
- package/templates/chat/src/main.ts +0 -3
package/README.md
CHANGED
|
@@ -12,20 +12,20 @@ npx @opentiny/tiny-robot-cli add chat --yes
|
|
|
12
12
|
|
|
13
13
|
## Options
|
|
14
14
|
|
|
15
|
-
- `-t, --template <name>`: template name; `basic` generates the Chat Basic project
|
|
16
|
-
- `--runtime-version <version>`: override the TinyRobot runtime version
|
|
15
|
+
- `-t, --template <name>`: template name; `basic` generates the Chat Basic project and `chat` generates the standalone Chat project
|
|
16
|
+
- `--runtime-version <version>`: override the TinyRobot runtime version used in the generated or updated project
|
|
17
17
|
- `-h, --help`: show help
|
|
18
18
|
|
|
19
|
-
`create`
|
|
19
|
+
`create` scaffolds a new project. The `basic` template creates the Chat Basic project, and the `chat` template creates a standalone Chat project.
|
|
20
20
|
|
|
21
|
-
`add chat`
|
|
21
|
+
`add chat` creates the isolated `src/tiny-robot-chat/` feature from `packages/cli/templates/chat`, adds the runtime dependencies (including `@vueuse/core@13.9.0`), and merges `.env.example`. Existing files with different contents are reported as conflicts and are never silently overwritten. If a write fails, the CLI attempts to roll back the changes that were already applied and reports any residual paths.
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
`add chat` never modifies `src/main.ts`, `src/main.js`, `src/App.vue`, or `vite.config.*`, and it does not mount the component automatically. Render `TinyRobotChat` in the host application and add the MCP proxy manually; the generated component imports its required styles. Interactive mode shows the plan and asks for confirmation; `--dry-run` previews the plan without changing files and `--yes` applies it without confirmation:
|
|
24
24
|
|
|
25
25
|
```bash
|
|
26
26
|
npx @opentiny/tiny-robot-cli add chat --dry-run
|
|
27
27
|
npx @opentiny/tiny-robot-cli add chat --yes
|
|
28
|
-
npx @opentiny/tiny-robot-cli add chat --yes --
|
|
28
|
+
npx @opentiny/tiny-robot-cli add chat --yes --runtime-version 0.5.2-rc.2
|
|
29
29
|
```
|
|
30
30
|
|
|
31
31
|
The feature adds or preserves these dependencies. Compatible versions are kept, and higher versions are not downgraded:
|
|
@@ -44,9 +44,9 @@ CLI 0.5.2-beta.3 -> runtime 0.5.2-beta.3
|
|
|
44
44
|
CLI 0.5.3 -> runtime ^0.5.3
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
-
An existing dependency range is preserved when it accepts the target runtime version; for example, `^0.5.1` is kept for a stable `0.5.3` target.
|
|
47
|
+
An existing stable dependency range is preserved when it accepts the target runtime version; for example, `^0.5.1` is kept for a stable `0.5.3` target. Prerelease targets always use the exact requested version. The override follows the same prerelease-exact and stable-caret rules.
|
|
48
48
|
|
|
49
|
-
Copy the generated `.env.example` to `.env.local`, then configure the provider API
|
|
49
|
+
Copy the generated `.env.example` to `.env.local`, then configure the provider API key before starting the project. Optional provider endpoints are configured with `VITE_QWEN_API_URL` and `VITE_DEEPSEEK_API_URL`; the MCP endpoint uses `VITE_AMAP_MCP_URL`. `add chat` does not create or modify `.env`. `VITE_*` values are embedded in the client bundle, so do not use production keys; production deployments must protect provider credentials behind a server-side proxy.
|
|
50
50
|
|
|
51
51
|
The Model Context MCP example uses `/modelcontextprotocol-mcp`. Add this proxy manually to the existing `vite.config.*` file under `server.proxy`, then restart Vite:
|
|
52
52
|
|
|
@@ -67,3 +67,4 @@ server: {
|
|
|
67
67
|
Template-specific features and environment variables are documented in each template directory, for example:
|
|
68
68
|
|
|
69
69
|
- `packages/cli/templates/basic/README.md`
|
|
70
|
+
- `packages/cli/templates/chat/README.md`
|
package/bin/commands/add.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { compileTemplate, parse } from '@vue/compiler-sfc'
|
|
1
|
+
import { confirm, select } from '@inquirer/prompts'
|
|
3
2
|
import { Argument } from 'commander'
|
|
4
3
|
import fs from 'node:fs'
|
|
5
4
|
import path from 'node:path'
|
|
@@ -21,19 +20,6 @@ import {
|
|
|
21
20
|
} from '../utils.js'
|
|
22
21
|
|
|
23
22
|
const CHAT_ADD_FEATURE_DIR = 'src/tiny-robot-chat'
|
|
24
|
-
const PACKAGE_STYLE_IMPORTS = [
|
|
25
|
-
"import '@opentiny/tiny-robot/dist/style.css'",
|
|
26
|
-
"import '@opentiny/tiny-robot-chat/dist/style.css'",
|
|
27
|
-
]
|
|
28
|
-
|
|
29
|
-
function logUnavailable(label) {
|
|
30
|
-
logSkip(`${label} could not be applied`)
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function logSkippedSelection(label) {
|
|
34
|
-
logSkip(`${label} change was not selected`)
|
|
35
|
-
}
|
|
36
|
-
|
|
37
23
|
async function resolveTargetPackage(cwd, nonInteractive) {
|
|
38
24
|
const workspaceRoot = findWorkspaceRoot(cwd)
|
|
39
25
|
|
|
@@ -110,54 +96,6 @@ function readPackageJson(pkgPath) {
|
|
|
110
96
|
return pkg
|
|
111
97
|
}
|
|
112
98
|
|
|
113
|
-
function findMainEntry(targetDir) {
|
|
114
|
-
for (const file of ['src/main.ts', 'src/main.js']) {
|
|
115
|
-
const fullPath = path.join(targetDir, file)
|
|
116
|
-
if (fs.existsSync(fullPath) && fs.statSync(fullPath).isFile()) return fullPath
|
|
117
|
-
}
|
|
118
|
-
return null
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function importedModule(line) {
|
|
122
|
-
const match = /^\s*import(?:\s+[\s\S]*?\sfrom\s+)?\s*['"]([^'"]+)['"]\s*;?\s*$/.exec(line)
|
|
123
|
-
return match?.[1] ?? null
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function insertImport(content, importStatement) {
|
|
127
|
-
const eol = content.includes('\r\n') ? '\r\n' : '\n'
|
|
128
|
-
const lines = content.replaceAll('\r\n', '\n').split('\n')
|
|
129
|
-
const moduleName = importedModule(importStatement)
|
|
130
|
-
if (moduleName && lines.some((line) => importedModule(line) === moduleName)) return content
|
|
131
|
-
|
|
132
|
-
let lastImportIndex = -1
|
|
133
|
-
for (let i = 0; i < lines.length; i++) {
|
|
134
|
-
if (/^\s*import\s/.test(lines[i])) {
|
|
135
|
-
lastImportIndex = i
|
|
136
|
-
continue
|
|
137
|
-
}
|
|
138
|
-
if (lastImportIndex !== -1) break
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
if (lastImportIndex === -1) lines.unshift(importStatement)
|
|
142
|
-
else lines.splice(lastImportIndex + 1, 0, importStatement)
|
|
143
|
-
return lines.join(eol)
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function ensureStyleImportsContent(before, featureStyleImport) {
|
|
147
|
-
let after = before
|
|
148
|
-
for (const styleImport of [...PACKAGE_STYLE_IMPORTS, featureStyleImport]) {
|
|
149
|
-
after = insertImport(after, styleImport)
|
|
150
|
-
}
|
|
151
|
-
return { type: after === before ? 'skipped' : 'inserted', content: after }
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function ensureStyleImports(mainFile, featureStyleImport) {
|
|
155
|
-
const before = fs.readFileSync(mainFile, 'utf-8')
|
|
156
|
-
const result = ensureStyleImportsContent(before, featureStyleImport)
|
|
157
|
-
if (result.type === 'inserted') fs.writeFileSync(mainFile, result.content)
|
|
158
|
-
return { type: result.type }
|
|
159
|
-
}
|
|
160
|
-
|
|
161
99
|
function findDependency(pkg, name) {
|
|
162
100
|
const matches = []
|
|
163
101
|
for (const section of ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies']) {
|
|
@@ -232,6 +170,26 @@ function ensureDependency(pkg, name, targetSpecifier) {
|
|
|
232
170
|
}
|
|
233
171
|
}
|
|
234
172
|
|
|
173
|
+
if (semver.prerelease(targetVersion) !== null) {
|
|
174
|
+
if (dependency.version === targetSpecifier)
|
|
175
|
+
return { type: 'skipped', section: dependency.section, version: dependency.version }
|
|
176
|
+
|
|
177
|
+
if (dependency.section !== 'dependencies') {
|
|
178
|
+
return {
|
|
179
|
+
type: 'conflict',
|
|
180
|
+
reason: `${name}@${dependency.version} in ${dependency.section} does not satisfy ${targetSpecifier}`,
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
pkg.dependencies[name] = targetSpecifier
|
|
185
|
+
return {
|
|
186
|
+
type: 'updated',
|
|
187
|
+
section: dependency.section,
|
|
188
|
+
from: dependency.version,
|
|
189
|
+
to: targetSpecifier,
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
235
193
|
const satisfies = semver.satisfies(targetVersion, dependency.version)
|
|
236
194
|
|
|
237
195
|
if (satisfies) return { type: 'skipped', section: dependency.section, version: dependency.version }
|
|
@@ -293,83 +251,6 @@ function getChatFeatureFiles(targetDir) {
|
|
|
293
251
|
return featureFiles
|
|
294
252
|
}
|
|
295
253
|
|
|
296
|
-
function parseMountTemplate(source, filename) {
|
|
297
|
-
const parsed = parse(source, { filename })
|
|
298
|
-
if (parsed.errors.length > 0 || !parsed.descriptor.template) {
|
|
299
|
-
return { type: 'manual', reason: 'App.vue template could not be parsed safely' }
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
const compiled = compileTemplate({
|
|
303
|
-
source: parsed.descriptor.template.content,
|
|
304
|
-
filename,
|
|
305
|
-
id: 'tiny-robot-cli-mount',
|
|
306
|
-
})
|
|
307
|
-
if (compiled.errors.length > 0 || !compiled.ast) {
|
|
308
|
-
return { type: 'manual', reason: 'App.vue template could not be parsed safely' }
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
return { type: 'parsed', block: parsed.descriptor.template, components: compiled.ast.components ?? [] }
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
function findTemplateClose(block) {
|
|
315
|
-
return block.loc.end.offset
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
function planMount(targetDir) {
|
|
319
|
-
const appFile = path.join(targetDir, 'src/App.vue')
|
|
320
|
-
if (!fs.existsSync(appFile)) return { type: 'manual', reason: 'src/App.vue was not found' }
|
|
321
|
-
if (!fs.statSync(appFile).isFile()) return { type: 'manual', reason: 'src/App.vue is not a file' }
|
|
322
|
-
const before = fs.readFileSync(appFile, 'utf-8')
|
|
323
|
-
|
|
324
|
-
const templatePlan = parseMountTemplate(before, appFile)
|
|
325
|
-
if (templatePlan.type === 'manual') return templatePlan
|
|
326
|
-
if (templatePlan.components.includes('TinyRobotChat')) return { type: 'skipped', content: before }
|
|
327
|
-
|
|
328
|
-
const importStatement = "import TinyRobotChat from './tiny-robot-chat/TinyRobotChat.vue'"
|
|
329
|
-
let after = before
|
|
330
|
-
const setupMatch = /<script\s+setup(?:\s[^>]*)?>([\s\S]*?)<\/script>/i.exec(before)
|
|
331
|
-
if (setupMatch) {
|
|
332
|
-
const setupContent = setupMatch[1]
|
|
333
|
-
const defaultImport = /^\s*import\s+TinyRobotChat\s+from\s+['"]([^'"]+)['"]\s*;?\s*$/m.exec(setupContent)
|
|
334
|
-
const featureImport =
|
|
335
|
-
/^\s*import\s+([A-Za-z_$][\w$]*)\s+from\s+['"]\.\/tiny-robot-chat\/TinyRobotChat\.vue['"]\s*;?\s*$/m.exec(
|
|
336
|
-
setupContent,
|
|
337
|
-
)
|
|
338
|
-
const namedImport = /^\s*import\s*\{[^}]*\bTinyRobotChat\b[^}]*\}\s*from\s+['"][^'"]+['"]\s*;?\s*$/m.test(
|
|
339
|
-
setupContent,
|
|
340
|
-
)
|
|
341
|
-
const localBinding = /^\s*(?:const|let|var|function|class)\s+TinyRobotChat\b/m.test(setupContent)
|
|
342
|
-
|
|
343
|
-
if (
|
|
344
|
-
namedImport ||
|
|
345
|
-
localBinding ||
|
|
346
|
-
(defaultImport && defaultImport[1] !== './tiny-robot-chat/TinyRobotChat.vue') ||
|
|
347
|
-
(featureImport && featureImport[1] !== 'TinyRobotChat')
|
|
348
|
-
) {
|
|
349
|
-
return { type: 'manual', reason: 'App.vue already declares TinyRobotChat with a different binding' }
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
if (!defaultImport) {
|
|
353
|
-
const insertionIndex = setupMatch.index + setupMatch[0].indexOf('>') + 1
|
|
354
|
-
after = `${before.slice(0, insertionIndex)}\n${importStatement}${before.slice(insertionIndex)}`
|
|
355
|
-
}
|
|
356
|
-
} else if (/<script(?:\s[^>]*)?>/.test(before)) {
|
|
357
|
-
return { type: 'manual', reason: 'App.vue has a non-setup script block' }
|
|
358
|
-
} else {
|
|
359
|
-
after = `<script setup lang="ts">\n${importStatement}\n</script>\n\n${before}`
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
const updatedTemplate = parseMountTemplate(after, appFile)
|
|
363
|
-
if (updatedTemplate.type === 'manual') return updatedTemplate
|
|
364
|
-
const templateClose = findTemplateClose(updatedTemplate.block)
|
|
365
|
-
if (templateClose === -1) return { type: 'manual', reason: 'App.vue template closing tag was not found' }
|
|
366
|
-
const beforeClose = after.slice(0, templateClose).trimEnd()
|
|
367
|
-
const lineIndent = /^\s*/.exec(beforeClose.slice(beforeClose.lastIndexOf('\n') + 1))?.[0] ?? ''
|
|
368
|
-
const indent = lineIndent || ' '
|
|
369
|
-
after = `${beforeClose}\n${indent}<TinyRobotChat />\n${after.slice(templateClose)}`
|
|
370
|
-
return { type: 'merge', content: after }
|
|
371
|
-
}
|
|
372
|
-
|
|
373
254
|
function inspectFeatureFiles(files) {
|
|
374
255
|
return files.map((file) => {
|
|
375
256
|
if (!fs.existsSync(file.target)) return { ...file, type: 'create' }
|
|
@@ -383,45 +264,6 @@ function formatRelative(file, targetDir) {
|
|
|
383
264
|
return path.relative(targetDir, file).replaceAll('\\', '/')
|
|
384
265
|
}
|
|
385
266
|
|
|
386
|
-
function getChatFeatureChoices(targetDir, options) {
|
|
387
|
-
const mainEntry = findMainEntry(targetDir)
|
|
388
|
-
return [
|
|
389
|
-
{ label: 'Chat feature files', enabled: true },
|
|
390
|
-
{ label: 'main entry style imports', enabled: Boolean(mainEntry) },
|
|
391
|
-
{ label: '.env.example', enabled: true },
|
|
392
|
-
{ label: 'package.json', enabled: true },
|
|
393
|
-
...(options.mount ? [{ label: 'App.vue mount', enabled: fs.existsSync(path.join(targetDir, 'src/App.vue')) }] : []),
|
|
394
|
-
].map((item) => ({ ...item, mainEntry }))
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
async function selectFileChanges(targetDir, options) {
|
|
398
|
-
const files = getChatFeatureChoices(targetDir, options)
|
|
399
|
-
if (options.yes || options.dryRun || options.nonInteractive)
|
|
400
|
-
return files.filter((file) => file.enabled).map((file) => file.label)
|
|
401
|
-
return checkbox({
|
|
402
|
-
message: 'Select which file changes to apply (all selected by default):',
|
|
403
|
-
choices: files.map((file) => ({
|
|
404
|
-
name: file.enabled ? file.label : `${file.label} (not available)`,
|
|
405
|
-
value: file.label,
|
|
406
|
-
checked: file.enabled,
|
|
407
|
-
disabled: !file.enabled,
|
|
408
|
-
})),
|
|
409
|
-
})
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
function isSelected(selectedFiles, label) {
|
|
413
|
-
return selectedFiles.includes(label)
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
function getDependencyPlan(pkg, dependencies) {
|
|
417
|
-
const preview = JSON.parse(JSON.stringify(pkg))
|
|
418
|
-
|
|
419
|
-
return Object.entries(dependencies).map(([name, version]) => ({
|
|
420
|
-
name,
|
|
421
|
-
result: ensureDependency(preview, name, version),
|
|
422
|
-
}))
|
|
423
|
-
}
|
|
424
|
-
|
|
425
267
|
function getEnvPlan(targetDir) {
|
|
426
268
|
const templateFile = getTemplateFile('.env.example')
|
|
427
269
|
const targetFile = path.join(targetDir, '.env.example')
|
|
@@ -436,7 +278,7 @@ function getEnvPlan(targetDir) {
|
|
|
436
278
|
|
|
437
279
|
function formatPlanStatus(type) {
|
|
438
280
|
if (type === 'create') return '+'
|
|
439
|
-
if (type === 'merge') return '~'
|
|
281
|
+
if (type === 'merge' || type === 'merged') return '~'
|
|
440
282
|
if (type === 'unavailable' || type === 'conflict') return '!'
|
|
441
283
|
return '○'
|
|
442
284
|
}
|
|
@@ -449,24 +291,40 @@ function fileContent(content) {
|
|
|
449
291
|
return Buffer.isBuffer(content) ? content : Buffer.from(content)
|
|
450
292
|
}
|
|
451
293
|
|
|
452
|
-
function
|
|
453
|
-
if (fs.existsSync(target))
|
|
294
|
+
function readTargetState(target) {
|
|
295
|
+
if (!fs.existsSync(target)) return { existed: false, content: null }
|
|
296
|
+
invariant(fs.statSync(target).isFile(), `${target} exists and is not a file.`)
|
|
297
|
+
return { existed: true, content: fs.readFileSync(target) }
|
|
298
|
+
}
|
|
454
299
|
|
|
300
|
+
function addFileChange(changes, target, content, message) {
|
|
301
|
+
const expected = readTargetState(target)
|
|
455
302
|
const next = fileContent(content)
|
|
456
|
-
if (
|
|
303
|
+
if (expected.existed && expected.content.equals(next)) return false
|
|
457
304
|
|
|
458
|
-
changes.push({ target, content, message })
|
|
305
|
+
changes.push({ target, content: next, message, expected })
|
|
459
306
|
return true
|
|
460
307
|
}
|
|
461
308
|
|
|
309
|
+
function assertTargetUnchanged(change) {
|
|
310
|
+
const current = readTargetState(change.target)
|
|
311
|
+
if (current.existed !== change.expected.existed) {
|
|
312
|
+
throw new Error(`planned target changed after planning: ${change.target}`)
|
|
313
|
+
}
|
|
314
|
+
if (current.existed && !current.content.equals(change.expected.content)) {
|
|
315
|
+
throw new Error(`planned target changed after planning: ${change.target}`)
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
462
319
|
function applyChanges(changes) {
|
|
463
320
|
const snapshots = changes.map(({ target }) => ({
|
|
464
321
|
target,
|
|
465
|
-
|
|
466
|
-
content: fs.existsSync(target) ? fs.readFileSync(target) : null,
|
|
322
|
+
...readTargetState(target),
|
|
467
323
|
}))
|
|
468
324
|
const createdDirectories = new Set()
|
|
469
325
|
|
|
326
|
+
for (const change of changes) assertTargetUnchanged(change)
|
|
327
|
+
|
|
470
328
|
try {
|
|
471
329
|
for (const change of changes) {
|
|
472
330
|
let directory = path.dirname(change.target)
|
|
@@ -480,12 +338,31 @@ function applyChanges(changes) {
|
|
|
480
338
|
fs.writeFileSync(change.target, change.content)
|
|
481
339
|
}
|
|
482
340
|
} catch (error) {
|
|
341
|
+
const rollbackErrors = []
|
|
483
342
|
for (const snapshot of snapshots.reverse()) {
|
|
484
|
-
|
|
485
|
-
|
|
343
|
+
try {
|
|
344
|
+
if (snapshot.existed) fs.writeFileSync(snapshot.target, snapshot.content)
|
|
345
|
+
else if (fs.existsSync(snapshot.target)) fs.rmSync(snapshot.target, { force: true })
|
|
346
|
+
} catch (rollbackError) {
|
|
347
|
+
rollbackErrors.push(
|
|
348
|
+
`${snapshot.target}: ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`,
|
|
349
|
+
)
|
|
350
|
+
}
|
|
486
351
|
}
|
|
487
352
|
for (const directory of [...createdDirectories].sort((left, right) => right.length - left.length)) {
|
|
488
|
-
|
|
353
|
+
try {
|
|
354
|
+
if (fs.existsSync(directory) && fs.readdirSync(directory).length === 0) fs.rmdirSync(directory)
|
|
355
|
+
} catch (rollbackError) {
|
|
356
|
+
rollbackErrors.push(
|
|
357
|
+
`${directory}: ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`,
|
|
358
|
+
)
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (rollbackErrors.length > 0) {
|
|
363
|
+
throw new Error(
|
|
364
|
+
`Failed to apply changes; rollback was incomplete: ${error instanceof Error ? error.message : String(error)}. Residual paths: ${rollbackErrors.join('; ')}`,
|
|
365
|
+
)
|
|
489
366
|
}
|
|
490
367
|
|
|
491
368
|
throw new Error(
|
|
@@ -494,155 +371,90 @@ function applyChanges(changes) {
|
|
|
494
371
|
}
|
|
495
372
|
}
|
|
496
373
|
|
|
497
|
-
function
|
|
498
|
-
const
|
|
499
|
-
const
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
throw new Error('Select Chat feature files before adding the local feature style import.')
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
if (!featureFilesSelected && featureFilesMissing && isSelected(selectedFiles, 'App.vue mount')) {
|
|
506
|
-
throw new Error('Select Chat feature files before mounting TinyRobotChat in App.vue.')
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
if (isSelected(selectedFiles, 'App.vue mount') && mountPlan?.type === 'manual' && !allowManualMount) {
|
|
510
|
-
throw new Error(`cannot safely mount TinyRobotChat: ${mountPlan.reason}`)
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
if (isSelected(selectedFiles, 'package.json')) return
|
|
374
|
+
function prepareChanges(targetDir, context) {
|
|
375
|
+
const { featureInspection, pkgPath, pkg, allowConflicts, dependencies } = context
|
|
376
|
+
const changes = []
|
|
377
|
+
const results = { featureInspection, env: null, dependencies: [], dependencyChanged: false }
|
|
378
|
+
const conflicts = featureInspection.filter((file) => file.type === 'conflict')
|
|
514
379
|
|
|
515
|
-
|
|
516
|
-
if (dependencyChanges.length > 0 && (featureFilesSelected || isSelected(selectedFiles, 'main entry style imports'))) {
|
|
380
|
+
if (conflicts.length > 0 && !allowConflicts) {
|
|
517
381
|
throw new Error(
|
|
518
|
-
|
|
382
|
+
`file conflicts detected:\n${conflicts.map((file) => ` - ${formatRelative(file.target, targetDir)}`).join('\n')}\nResolve the conflicts and run add chat again.`,
|
|
519
383
|
)
|
|
520
384
|
}
|
|
521
|
-
}
|
|
522
385
|
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
if (conflicts.length > 0 && !allowConflicts) {
|
|
531
|
-
throw new Error(
|
|
532
|
-
`file conflicts detected:\n${conflicts.map((file) => ` - ${formatRelative(file.target, targetDir)}`).join('\n')}\nResolve the conflicts and run add chat again.`,
|
|
533
|
-
)
|
|
534
|
-
}
|
|
535
|
-
for (const file of featureInspection) {
|
|
536
|
-
if (file.type === 'create')
|
|
537
|
-
addFileChange(
|
|
538
|
-
changes,
|
|
539
|
-
file.target,
|
|
540
|
-
fs.readFileSync(file.source),
|
|
541
|
-
`Created ${formatRelative(file.target, targetDir)}`,
|
|
542
|
-
)
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
if (isSelected(selectedFiles, 'main entry style imports')) {
|
|
547
|
-
if (!mainFile) results.style = { type: 'unavailable' }
|
|
548
|
-
else {
|
|
549
|
-
const styleResult = ensureStyleImportsContent(
|
|
550
|
-
fs.readFileSync(mainFile, 'utf-8'),
|
|
551
|
-
"import './tiny-robot-chat/index.css'",
|
|
386
|
+
for (const file of featureInspection) {
|
|
387
|
+
if (file.type === 'create')
|
|
388
|
+
addFileChange(
|
|
389
|
+
changes,
|
|
390
|
+
file.target,
|
|
391
|
+
fs.readFileSync(file.source),
|
|
392
|
+
`Created ${formatRelative(file.target, targetDir)}`,
|
|
552
393
|
)
|
|
553
|
-
results.style = styleResult
|
|
554
|
-
if (styleResult.type === 'inserted')
|
|
555
|
-
addFileChange(changes, mainFile, styleResult.content, 'Inserted TinyRobot and Chat feature style imports')
|
|
556
|
-
}
|
|
557
394
|
}
|
|
558
395
|
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
addFileChange(changes, results.env.targetFile, results.env.content, `Added ${results.env.added} env variables`)
|
|
565
|
-
}
|
|
396
|
+
results.env = getEnvPlan(targetDir)
|
|
397
|
+
if (results.env.type === 'create')
|
|
398
|
+
addFileChange(changes, results.env.targetFile, results.env.content, 'Created .env.example')
|
|
399
|
+
if (results.env.type === 'merged')
|
|
400
|
+
addFileChange(changes, results.env.targetFile, results.env.content, `Added ${results.env.added} env variables`)
|
|
566
401
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
results.dependencyChanged ||= result.type !== 'skipped'
|
|
573
|
-
}
|
|
574
|
-
if (results.dependencyChanged)
|
|
575
|
-
addFileChange(changes, pkgPath, `${JSON.stringify(pkg, null, 2)}\n`, 'Updated package.json')
|
|
402
|
+
for (const [name, version] of Object.entries(dependencies)) {
|
|
403
|
+
const result = ensureDependency(pkg, name, version)
|
|
404
|
+
if (result.type === 'conflict' && !allowConflicts) throw new Error(`${name}: ${result.reason}`)
|
|
405
|
+
results.dependencies.push({ name, result })
|
|
406
|
+
results.dependencyChanged ||= result.type !== 'skipped'
|
|
576
407
|
}
|
|
577
408
|
|
|
578
|
-
if (
|
|
579
|
-
|
|
580
|
-
if (mountPlan.type === 'merge')
|
|
581
|
-
addFileChange(
|
|
582
|
-
changes,
|
|
583
|
-
path.join(targetDir, 'src/App.vue'),
|
|
584
|
-
mountPlan.content,
|
|
585
|
-
'Mounted TinyRobotChat in src/App.vue',
|
|
586
|
-
)
|
|
587
|
-
}
|
|
409
|
+
if (results.dependencyChanged)
|
|
410
|
+
addFileChange(changes, pkgPath, `${JSON.stringify(pkg, null, 2)}\n`, 'Updated package.json')
|
|
588
411
|
|
|
589
412
|
return { changes, results }
|
|
590
413
|
}
|
|
591
414
|
|
|
592
|
-
function
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
else logSkip('TinyRobot and Chat feature style imports already exist')
|
|
604
|
-
} else logSkippedSelection('main entry style imports')
|
|
605
|
-
|
|
606
|
-
if (isSelected(selectedFiles, '.env.example')) {
|
|
607
|
-
if (results.env?.type === 'unavailable') logUnavailable('.env.example (template .env.example not found)')
|
|
608
|
-
else if (results.env?.type === 'create') logSuccess('Created .env.example')
|
|
609
|
-
else if (results.env?.type === 'merged') logSuccess(`Added ${results.env.added} env variables`)
|
|
610
|
-
else logSkip('.env.example already contains required variables')
|
|
611
|
-
} else logSkippedSelection('.env.example')
|
|
612
|
-
|
|
613
|
-
if (isSelected(selectedFiles, 'package.json')) {
|
|
614
|
-
for (const { name, result } of results.dependencies) printDependencyResult(result, name)
|
|
615
|
-
if (!results.dependencyChanged) logSkip('package.json already contains required dependencies')
|
|
616
|
-
} else logSkippedSelection('package.json')
|
|
617
|
-
|
|
618
|
-
if (isSelected(selectedFiles, 'App.vue mount')) {
|
|
619
|
-
if (results.mount?.type === 'merge') logSuccess('Mounted TinyRobotChat in src/App.vue')
|
|
620
|
-
else logSkip('App.vue already mounts TinyRobotChat')
|
|
415
|
+
function printChangePlan(targetDir, results) {
|
|
416
|
+
console.log('\nChange Plan\n')
|
|
417
|
+
for (const file of results.featureInspection) {
|
|
418
|
+
console.log(` ${formatPlanStatus(file.type)} ${formatRelative(file.target, targetDir)}`)
|
|
419
|
+
}
|
|
420
|
+
console.log(
|
|
421
|
+
` ${formatPlanStatus(results.env?.type ?? 'unavailable')} .env.example (${results.env?.type ?? 'unavailable'})`,
|
|
422
|
+
)
|
|
423
|
+
for (const { name, result } of results.dependencies) {
|
|
424
|
+
const status = result.type === 'added' || result.type === 'updated' ? '~' : result.type === 'conflict' ? '!' : '○'
|
|
425
|
+
console.log(` ${status} package.json (${result.type}: ${name})`)
|
|
621
426
|
}
|
|
427
|
+
console.log(' ○ src/main.ts / src/main.js (unchanged)')
|
|
428
|
+
console.log(' ○ src/App.vue (unchanged)')
|
|
429
|
+
console.log(' ○ vite.config.* (unchanged)')
|
|
430
|
+
printManualMcpSetup()
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function printChangeResults(targetDir, results) {
|
|
434
|
+
for (const file of results.featureInspection) {
|
|
435
|
+
if (file.type === 'create') logSuccess(`Created ${formatRelative(file.target, targetDir)}`)
|
|
436
|
+
else if (file.type === 'skipped') logSkip(`${formatRelative(file.target, targetDir)} already exists`)
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (results.env?.type === 'create') logSuccess('Created .env.example')
|
|
440
|
+
else if (results.env?.type === 'merged') logSuccess(`Added ${results.env.added} env variables`)
|
|
441
|
+
else if (results.env?.type === 'unavailable') logSkip('.env.example template is unavailable; no environment variables were added')
|
|
442
|
+
else logSkip('.env.example already contains required variables')
|
|
443
|
+
|
|
444
|
+
for (const { name, result } of results.dependencies) printDependencyResult(result, name)
|
|
445
|
+
if (!results.dependencyChanged) logSkip('package.json already contains required dependencies')
|
|
622
446
|
}
|
|
623
447
|
|
|
624
448
|
async function addFeature(targetDir, type, options) {
|
|
625
449
|
invariant(type === 'chat', `unsupported feature: ${type}`)
|
|
626
|
-
const mountRequested = options.mount !== false
|
|
627
450
|
const featureFiles = getChatFeatureFiles(targetDir)
|
|
628
|
-
const mainFile = findMainEntry(targetDir)
|
|
629
|
-
const mountPlan = mountRequested ? planMount(targetDir) : null
|
|
630
451
|
const pkgPath = path.join(targetDir, 'package.json')
|
|
631
452
|
invariant(fs.existsSync(pkgPath), 'package.json not found.')
|
|
632
453
|
invariant(fs.statSync(pkgPath).isFile(), 'package.json is not a file.')
|
|
633
454
|
const pkg = readPackageJson(pkgPath)
|
|
634
455
|
const featureInspection = inspectFeatureFiles(featureFiles)
|
|
635
|
-
const
|
|
636
|
-
...options,
|
|
637
|
-
mount: mountRequested,
|
|
638
|
-
nonInteractive: options.nonInteractive,
|
|
639
|
-
})
|
|
640
|
-
|
|
641
|
-
validateSelection(selectedFiles, pkg, featureInspection, mountPlan, options.dryRun, options.dependencies)
|
|
642
|
-
const prepared = prepareChanges(targetDir, selectedFiles, {
|
|
456
|
+
const prepared = prepareChanges(targetDir, {
|
|
643
457
|
featureInspection,
|
|
644
|
-
mainFile,
|
|
645
|
-
mountPlan,
|
|
646
458
|
pkgPath,
|
|
647
459
|
pkg,
|
|
648
460
|
allowConflicts: options.dryRun,
|
|
@@ -650,77 +462,51 @@ async function addFeature(targetDir, type, options) {
|
|
|
650
462
|
})
|
|
651
463
|
|
|
652
464
|
if (options.dryRun) {
|
|
653
|
-
|
|
654
|
-
for (const file of featureInspection)
|
|
655
|
-
console.log(
|
|
656
|
-
` ${file.type === 'create' ? '+' : file.type === 'conflict' ? '!' : '○'} ${formatRelative(file.target, targetDir)}`,
|
|
657
|
-
)
|
|
658
|
-
console.log(
|
|
659
|
-
` ${mainFile ? formatPlanStatus(prepared.results.style?.type) : '!'} ${mainFile ? formatRelative(mainFile, targetDir) : 'src/main.ts or src/main.js'} (style imports)`,
|
|
660
|
-
)
|
|
661
|
-
console.log(
|
|
662
|
-
` ${formatPlanStatus(prepared.results.env?.type ?? 'unavailable')} .env.example (${prepared.results.env?.type ?? 'not selected'})`,
|
|
663
|
-
)
|
|
664
|
-
const dependencyPlan =
|
|
665
|
-
prepared.results.dependencies.length > 0
|
|
666
|
-
? prepared.results.dependencies
|
|
667
|
-
: getDependencyPlan(pkg, options.dependencies)
|
|
668
|
-
for (const { name, result } of dependencyPlan) {
|
|
669
|
-
const status = result.type === 'added' || result.type === 'updated' ? '~' : result.type === 'conflict' ? '!' : '○'
|
|
670
|
-
console.log(` ${status} package.json (${result.type}: ${name})`)
|
|
671
|
-
}
|
|
672
|
-
if (mountPlan)
|
|
673
|
-
console.log(
|
|
674
|
-
` ${mountPlan.type === 'merge' ? '~' : mountPlan.type === 'manual' ? '!' : '○'} src/App.vue (mount: ${mountPlan.type})`,
|
|
675
|
-
)
|
|
676
|
-
printManualMcpSetup()
|
|
465
|
+
printChangePlan(targetDir, prepared.results)
|
|
677
466
|
return
|
|
678
467
|
}
|
|
679
468
|
|
|
680
|
-
if (
|
|
681
|
-
logSkip('No changes
|
|
469
|
+
if (prepared.changes.length === 0) {
|
|
470
|
+
logSkip('No changes were necessary.')
|
|
682
471
|
return
|
|
683
472
|
}
|
|
473
|
+
|
|
474
|
+
if (!options.yes && !options.nonInteractive) {
|
|
475
|
+
printChangePlan(targetDir, prepared.results)
|
|
476
|
+
const accepted = await confirm({ message: `Apply these changes to ${targetDir}?`, default: true })
|
|
477
|
+
if (!accepted) {
|
|
478
|
+
logSkip('Operation cancelled.')
|
|
479
|
+
return
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
684
483
|
console.log('\nChange Results\n')
|
|
685
484
|
applyChanges(prepared.changes)
|
|
686
|
-
printChangeResults(targetDir,
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
printNextSteps({
|
|
690
|
-
mainFile,
|
|
691
|
-
dependencyChanged: prepared.results.dependencyChanged,
|
|
692
|
-
mounted: isSelected(selectedFiles, 'App.vue mount') && mountPlan?.type !== 'manual',
|
|
693
|
-
selectedFiles,
|
|
694
|
-
featureFilesSelected: isSelected(selectedFiles, 'Chat feature files'),
|
|
695
|
-
})
|
|
485
|
+
printChangeResults(targetDir, prepared.results)
|
|
486
|
+
console.log(`\nSuccessfully added "${type}" feature to ${targetDir}`)
|
|
487
|
+
printNextSteps({ dependencyChanged: prepared.results.dependencyChanged })
|
|
696
488
|
}
|
|
697
489
|
|
|
698
|
-
function printNextSteps({
|
|
490
|
+
function printNextSteps({ dependencyChanged }) {
|
|
699
491
|
const steps = []
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
)
|
|
719
|
-
}
|
|
720
|
-
if (isSelected(selectedFiles, '.env.example'))
|
|
721
|
-
steps.push('Copy .env.example to .env.local and configure your AI provider API keys.')
|
|
722
|
-
if (featureFilesSelected)
|
|
723
|
-
steps.push('Add the Model Context MCP proxy to vite.config.* under server.proxy, then restart Vite.')
|
|
492
|
+
steps.push(
|
|
493
|
+
[
|
|
494
|
+
'Render <TinyRobotChat /> near your main application component.',
|
|
495
|
+
'',
|
|
496
|
+
"Example ('src/App.vue'):",
|
|
497
|
+
'',
|
|
498
|
+
' <script setup lang="ts">',
|
|
499
|
+
" import TinyRobotChat from './tiny-robot-chat/TinyRobotChat.vue'",
|
|
500
|
+
' </script>',
|
|
501
|
+
'',
|
|
502
|
+
' <template>',
|
|
503
|
+
' <YourAppComponent />',
|
|
504
|
+
' <TinyRobotChat />',
|
|
505
|
+
' </template>',
|
|
506
|
+
].join('\n'),
|
|
507
|
+
)
|
|
508
|
+
steps.push('Copy .env.example to .env.local and configure your AI provider API keys.')
|
|
509
|
+
steps.push('Add the Model Context MCP proxy to vite.config.* under server.proxy, then restart Vite.')
|
|
724
510
|
if (dependencyChanged) steps.push('Install or update project dependencies: pnpm install')
|
|
725
511
|
if (steps.length > 0) {
|
|
726
512
|
console.log('\nNext Steps\n')
|
|
@@ -733,10 +519,8 @@ export function registerAddCommand(program) {
|
|
|
733
519
|
.command('add')
|
|
734
520
|
.description('Add a feature to the project')
|
|
735
521
|
.addArgument(new Argument('<type>', 'type of feature to add').choices(['chat']))
|
|
736
|
-
.option('--yes', 'apply
|
|
522
|
+
.option('--yes', 'apply the complete chat feature without prompts')
|
|
737
523
|
.option('--dry-run', 'print the change plan without modifying files')
|
|
738
|
-
.option('--mount', 'safely mount TinyRobotChat in src/App.vue (default)')
|
|
739
|
-
.option('--no-mount', 'keep App.vue unchanged and print the mount snippet')
|
|
740
524
|
.option('--runtime-version <version>', 'override the TinyRobot runtime version')
|
|
741
525
|
.action(async (type, options) => {
|
|
742
526
|
try {
|
|
@@ -759,4 +543,4 @@ export function registerAddCommand(program) {
|
|
|
759
543
|
})
|
|
760
544
|
}
|
|
761
545
|
|
|
762
|
-
export {
|
|
546
|
+
export { addFileChange, applyChanges, ensureDependency, getChatFeatureFiles, printChangeResults, resolveTargetPackage }
|
package/bin/utils.js
CHANGED
|
@@ -7,7 +7,7 @@ import yaml from 'yaml'
|
|
|
7
7
|
const TEMPLATE_PLACEHOLDER = '__PROJECT_NAME__'
|
|
8
8
|
const RUNTIME_VERSION_PLACEHOLDER = '__TINY_ROBOT_VERSION__'
|
|
9
9
|
|
|
10
|
-
export const BUILTIN_TEMPLATES = ['basic']
|
|
10
|
+
export const BUILTIN_TEMPLATES = ['basic', 'chat']
|
|
11
11
|
export const DEFAULT_TEMPLATE = 'basic'
|
|
12
12
|
export const DEFAULT_PROJECT_NAME = 'tiny-robot-app'
|
|
13
13
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opentiny/tiny-robot-cli",
|
|
3
|
-
"version": "1.0.0-
|
|
3
|
+
"version": "1.0.0-rc.0",
|
|
4
4
|
"description": "CLI to scaffold TinyRobot product projects",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -28,11 +28,10 @@
|
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@inquirer/prompts": "^8.3.2",
|
|
31
|
-
"@vue/compiler-sfc": "^3.5.40",
|
|
32
31
|
"commander": "^14.0.3",
|
|
33
32
|
"picocolors": "^1.1.1",
|
|
34
33
|
"semver": "^7.8.1",
|
|
35
34
|
"yaml": "^2.9.0"
|
|
36
35
|
},
|
|
37
|
-
"gitHead": "
|
|
36
|
+
"gitHead": "a2ee45f7628dfd1cacdddd159050c026ba11e67e"
|
|
38
37
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import {
|
|
3
3
|
TrChat,
|
|
4
|
-
|
|
4
|
+
useChatRuntime,
|
|
5
5
|
type ChatMcpServers,
|
|
6
6
|
type ChatProviderConfig,
|
|
7
7
|
type ChatUIOptions,
|
|
@@ -90,7 +90,7 @@ const mcpServers = [
|
|
|
90
90
|
},
|
|
91
91
|
] satisfies ChatMcpServers
|
|
92
92
|
|
|
93
|
-
const runtime =
|
|
93
|
+
const runtime = useChatRuntime({
|
|
94
94
|
mcpServers,
|
|
95
95
|
modelProviders,
|
|
96
96
|
})
|
|
@@ -5,6 +5,7 @@ VITE_DEEPSEEK_API_URL=
|
|
|
5
5
|
# Optional custom MCP endpoint. Leave blank to use the official endpoint.
|
|
6
6
|
VITE_AMAP_MCP_URL=
|
|
7
7
|
|
|
8
|
-
# Optional local direct-connection credentials.
|
|
8
|
+
# Optional local direct-connection credentials (development only).
|
|
9
|
+
# WARNING: VITE_* values are embedded in the client bundle. Do not use production keys.
|
|
9
10
|
VITE_ALIYUN_DASHSCOPE_KEY=
|
|
10
11
|
VITE_DEEPSEEK_API_KEY=
|
package/templates/chat/README.md
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
# TinyRobot Chat Add
|
|
2
2
|
|
|
3
|
-
这是一个独立的 Chat
|
|
3
|
+
这是一个独立的 Chat 项目模板,也可以通过 `add chat` 注入到现有 Vue 项目。
|
|
4
|
+
|
|
5
|
+
创建独立 Chat 项目:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx @opentiny/tiny-robot-cli create my-chat --template chat
|
|
9
|
+
```
|
|
4
10
|
|
|
5
11
|
## 启动
|
|
6
12
|
|
|
@@ -17,7 +23,9 @@ pnpm build
|
|
|
17
23
|
|
|
18
24
|
## 接入真实模型
|
|
19
25
|
|
|
20
|
-
复制项目根目录的 `.env.example` 为 `.env.local`,配置 `
|
|
26
|
+
复制项目根目录的 `.env.example` 为 `.env.local`,配置 `VITE_ALIYUN_DASHSCOPE_KEY` 和 `VITE_DEEPSEEK_API_KEY`,重启 Vite 后即可使用模型选择器中的真实模型。使用自定义 Provider 代理时,可配置 `VITE_QWEN_API_URL` 和 `VITE_DEEPSEEK_API_URL`;自定义 MCP 地址使用 `VITE_AMAP_MCP_URL`。模板只提供 `.env.example`,不会自动创建 `.env`。配置读取逻辑位于 `src/config/chat-runtime.ts`;使用 `add chat` 注入后,对应文件位于 `src/tiny-robot-chat/config/chat-runtime.ts`。
|
|
27
|
+
|
|
28
|
+
`VITE_*` 变量会被写入浏览器产物,仅限开发使用,禁止配置生产密钥;生产环境应通过服务端代理保护 Provider 凭证。
|
|
21
29
|
|
|
22
30
|
## 模型配置
|
|
23
31
|
|
|
@@ -33,6 +41,7 @@ DeepSeek 模型默认继承 `low`、`high`、`max` 三档思考强度,默认
|
|
|
33
41
|
|
|
34
42
|
- `src/config/chat-ui.ts`: Chat UI、提示词、模板和菜单配置。
|
|
35
43
|
- `src/config/chat-runtime.ts`: MCP 示例和模型配置。
|
|
44
|
+
- `src/TinyRobotChat.vue`: Chat 入口组件及其样式依赖。
|
|
36
45
|
- `src/components`: 窗口头部和输入区工具。
|
|
37
46
|
- `src/composables/useWindow.ts`: floating、fullscreen 窗口状态。
|
|
38
47
|
- `src/index.css`: 仅作用于 Chat 容器的组件样式和 Surface 样式变量,不重置宿主页面的全局样式。
|
|
@@ -1,7 +1,7 @@
|
|
|
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 { TrChat, useChatHistoryItems,
|
|
4
|
+
import { TrChat, useChatHistoryItems, useChatRuntime, type ChatHistoryItem } from '@opentiny/tiny-robot-chat'
|
|
5
5
|
import { IconAi } from '@opentiny/tiny-robot-svgs'
|
|
6
6
|
import { computed, reactive, ref } from 'vue'
|
|
7
7
|
import ComposerTools from './components/ComposerTools.vue'
|
|
@@ -9,8 +9,11 @@ import WindowHeader from './components/WindowHeader.vue'
|
|
|
9
9
|
import { composerMenus, createChatUi, templateCategories } from './config/chat-ui'
|
|
10
10
|
import { mcpExamples, mcpServers, modelProviders } from './config/chat-runtime'
|
|
11
11
|
import { useWindow } from './composables/useWindow'
|
|
12
|
+
import '@opentiny/tiny-robot/dist/style.css'
|
|
13
|
+
import '@opentiny/tiny-robot-chat/dist/style.css'
|
|
14
|
+
import './index.css'
|
|
12
15
|
|
|
13
|
-
const runtime =
|
|
16
|
+
const runtime = useChatRuntime({ modelProviders, mcpServers })
|
|
14
17
|
const window = reactive(useWindow())
|
|
15
18
|
const chatRef = ref<{ send: (payload: { text: string }) => Promise<boolean> } | null>(null)
|
|
16
19
|
const currentTemplate = ref<TemplateItem[]>([])
|