@opentiny/tiny-robot-cli 0.5.2-alpha.3 → 1.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/README.md +49 -1
  2. package/bin/commands/add.js +563 -269
  3. package/bin/commands/create.js +6 -3
  4. package/bin/runtime-version.js +37 -0
  5. package/bin/utils.js +147 -45
  6. package/package.json +5 -3
  7. package/templates/basic/.env.example +8 -0
  8. package/templates/basic/README.md +53 -22
  9. package/templates/basic/index.html +2 -2
  10. package/templates/basic/package.json +12 -16
  11. package/templates/basic/public/favicon.svg +1 -0
  12. package/templates/basic/public/icons.svg +24 -0
  13. package/templates/basic/src/App.vue +111 -119
  14. package/templates/basic/src/main.ts +2 -2
  15. package/templates/basic/tsconfig.app.json +2 -5
  16. package/templates/basic/tsconfig.node.json +3 -6
  17. package/templates/basic/vite.config.ts +6 -1
  18. package/templates/chat/.env.example +9 -0
  19. package/templates/chat/README.md +46 -0
  20. package/templates/chat/index.html +12 -0
  21. package/templates/chat/package.json +28 -0
  22. package/templates/chat/src/TinyRobotChat.vue +88 -27
  23. package/templates/chat/src/components/ComposerTools.vue +99 -0
  24. package/templates/chat/src/components/WindowHeader.vue +192 -0
  25. package/templates/chat/src/composables/useWindow.ts +83 -0
  26. package/templates/chat/src/config/chat-runtime.ts +82 -0
  27. package/templates/chat/src/config/chat-ui.ts +289 -0
  28. package/templates/chat/src/index.css +109 -0
  29. package/templates/chat/src/main.ts +7 -0
  30. package/templates/chat/tsconfig.app.json +13 -0
  31. package/templates/chat/tsconfig.json +7 -0
  32. package/templates/chat/tsconfig.node.json +19 -0
  33. package/templates/chat/vite.config.ts +16 -0
  34. package/templates/basic/public/modelcontextprotocol.png +0 -0
  35. package/templates/basic/src/components/ChatList.vue +0 -82
  36. package/templates/basic/src/components/ChatSender.vue +0 -125
  37. package/templates/basic/src/components/ConversationHistory.vue +0 -136
  38. package/templates/basic/src/components/HistoryDrawerButton.vue +0 -43
  39. package/templates/basic/src/components/McpServerPickerButton.vue +0 -278
  40. package/templates/basic/src/components/ThemeToggleButton.vue +0 -44
  41. package/templates/basic/src/components/icons/IconDeepThink.vue +0 -29
  42. package/templates/basic/src/components/icons/IconModelAliyunBailian.vue +0 -51
  43. package/templates/basic/src/components/icons/IconModelDeepseek.vue +0 -29
  44. package/templates/basic/src/components/icons/IconMoon.vue +0 -29
  45. package/templates/basic/src/components/icons/IconPlugin.vue +0 -29
  46. package/templates/basic/src/components/icons/IconSun.vue +0 -35
  47. package/templates/basic/src/components/icons/IconWebSearch.vue +0 -36
  48. package/templates/basic/src/components/icons/index.ts +0 -7
  49. package/templates/basic/src/composables/useChat.ts +0 -129
  50. package/templates/basic/src/composables/useMcp.ts +0 -170
  51. package/templates/basic/src/composables/useModel.ts +0 -82
  52. package/templates/basic/src/mcpServers.ts +0 -40
  53. package/templates/basic/src/models.ts +0 -81
@@ -2,6 +2,7 @@ import { input, select } from '@inquirer/prompts'
2
2
  import path from 'node:path'
3
3
  import process from 'node:process'
4
4
 
5
+ import { resolveRuntimeVersion } from '../runtime-version.js'
5
6
  import {
6
7
  DEFAULT_PROJECT_NAME,
7
8
  DEFAULT_TEMPLATE,
@@ -119,12 +120,13 @@ function printCreateSuccess(projectName) {
119
120
  console.log()
120
121
  }
121
122
 
122
- async function createProject(initialProjectName, initialTemplateName, skipPrompt) {
123
+ async function createProject(initialProjectName, initialTemplateName, runtimeVersion, skipPrompt) {
124
+ const runtime = resolveRuntimeVersion(runtimeVersion)
123
125
  const options = await resolveCreateOptions(initialProjectName, initialTemplateName, skipPrompt)
124
126
 
125
127
  const { templateDir, targetDir } = validateCreateOptions(options)
126
128
 
127
- scaffoldProject(templateDir, targetDir, options.projectName)
129
+ scaffoldProject(templateDir, targetDir, options.projectName, runtime.specifier)
128
130
 
129
131
  printCreateSuccess(options.projectName)
130
132
  }
@@ -134,10 +136,11 @@ export function registerCreateCommand(program) {
134
136
  .command('create [project-name]')
135
137
  .description('Create a TinyRobot project from template')
136
138
  .option('-t, --template <name>', 'template name')
139
+ .option('--runtime-version <version>', 'override the TinyRobot runtime version')
137
140
  .action((projectName, options) => {
138
141
  const skipPrompt = !process.stdout.isTTY
139
142
 
140
- createProject(projectName ?? '', options.template ?? '', skipPrompt).catch((error) => {
143
+ createProject(projectName ?? '', options.template ?? '', options.runtimeVersion, skipPrompt).catch((error) => {
141
144
  if (error instanceof Error && error.name === 'ExitPromptError') {
142
145
  console.error('\nOperation cancelled.')
143
146
  process.exit(1)
@@ -0,0 +1,37 @@
1
+ import fs from 'node:fs'
2
+ import { fileURLToPath } from 'node:url'
3
+ import semver from 'semver'
4
+
5
+ const CLI_PACKAGE_FILE = fileURLToPath(new URL('../package.json', import.meta.url))
6
+
7
+ export function readCliVersion() {
8
+ const packageJson = JSON.parse(fs.readFileSync(CLI_PACKAGE_FILE, 'utf8'))
9
+ return packageJson.version
10
+ }
11
+
12
+ export function formatRuntimeSpecifier(version) {
13
+ const parsedVersion = typeof version === 'string' ? semver.parse(version) : null
14
+ const canonicalVersion = parsedVersion
15
+ ? `${parsedVersion.version}${parsedVersion.build.length > 0 ? `+${parsedVersion.build.join('.')}` : ''}`
16
+ : null
17
+ if (!parsedVersion || canonicalVersion !== version) {
18
+ throw new Error(`Runtime version must be a valid semantic version: ${String(version)}`)
19
+ }
20
+
21
+ return parsedVersion.prerelease.length > 0 ? version : `^${version}`
22
+ }
23
+
24
+ export function resolveRuntimeVersion(override) {
25
+ const version = override ?? readCliVersion()
26
+ return { version, specifier: formatRuntimeSpecifier(version) }
27
+ }
28
+
29
+ export function createRuntimeDependencies(specifier) {
30
+ return {
31
+ '@opentiny/tiny-robot': specifier,
32
+ '@opentiny/tiny-robot-chat': specifier,
33
+ '@opentiny/tiny-robot-kit': specifier,
34
+ '@opentiny/tiny-robot-svgs': specifier,
35
+ '@vueuse/core': '13.9.0',
36
+ }
37
+ }
package/bin/utils.js CHANGED
@@ -5,6 +5,7 @@ import pc from 'picocolors'
5
5
  import yaml from 'yaml'
6
6
 
7
7
  const TEMPLATE_PLACEHOLDER = '__PROJECT_NAME__'
8
+ const RUNTIME_VERSION_PLACEHOLDER = '__TINY_ROBOT_VERSION__'
8
9
 
9
10
  export const BUILTIN_TEMPLATES = ['basic']
10
11
  export const DEFAULT_TEMPLATE = 'basic'
@@ -36,10 +37,7 @@ export function logError(message) {
36
37
  }
37
38
 
38
39
  export function invariant(condition, message) {
39
- if (!condition) {
40
- console.error(`Error: ${message}`)
41
- process.exit(1)
42
- }
40
+ if (!condition) throw new Error(message)
43
41
  }
44
42
 
45
43
  export function exists(file) {
@@ -88,6 +86,10 @@ export function validateProjectName(name) {
88
86
  }
89
87
 
90
88
  export function copyTemplate(sourceDir, targetDir) {
89
+ copyDirectory(sourceDir, targetDir)
90
+ }
91
+
92
+ export function copyDirectory(sourceDir, targetDir) {
91
93
  fs.cpSync(sourceDir, targetDir, {
92
94
  recursive: true,
93
95
  filter: (source) => {
@@ -110,7 +112,7 @@ function renameSpecialFiles(targetDir) {
110
112
  }
111
113
 
112
114
  function replaceTemplateVariables(targetDir, variables) {
113
- const replaceFiles = ['package.json', 'README.md']
115
+ const replaceFiles = ['package.json', 'README.md', 'index.html']
114
116
 
115
117
  for (const relativePath of replaceFiles) {
116
118
  const file = path.join(targetDir, relativePath)
@@ -129,13 +131,14 @@ function replaceTemplateVariables(targetDir, variables) {
129
131
  }
130
132
  }
131
133
 
132
- export function scaffoldProject(templateDir, targetDir, projectName) {
134
+ export function scaffoldProject(templateDir, targetDir, projectName, runtimeVersion) {
133
135
  copyTemplate(templateDir, targetDir)
134
136
 
135
137
  renameSpecialFiles(targetDir)
136
138
 
137
139
  replaceTemplateVariables(targetDir, {
138
140
  [TEMPLATE_PLACEHOLDER]: projectName,
141
+ [RUNTIME_VERSION_PLACEHOLDER]: runtimeVersion,
139
142
  })
140
143
  }
141
144
 
@@ -166,20 +169,24 @@ export function findProjectRoot(cwd) {
166
169
  }
167
170
 
168
171
  export function findSubPackageRoot(cwd, workspaceRoot) {
169
- return findUp(cwd, (dir) => {
170
- if (dir === workspaceRoot) {
171
- return false
172
- }
172
+ let dir = path.resolve(cwd)
173
+ const root = path.resolve(workspaceRoot)
173
174
 
174
- return isPackageDir(dir)
175
- })
175
+ for (;;) {
176
+ if (dir === root) return null
177
+ if (isPackageDir(dir)) return dir
178
+
179
+ const parent = path.dirname(dir)
180
+ if (parent === dir) return null
181
+ dir = parent
182
+ }
176
183
  }
177
184
 
178
185
  export function findWorkspacePackages(workspaceRoot) {
179
186
  const workspaceFile = resolveWorkspaceFile(workspaceRoot)
180
187
 
181
188
  if (!workspaceFile) {
182
- return []
189
+ return undefined
183
190
  }
184
191
 
185
192
  try {
@@ -187,56 +194,139 @@ export function findWorkspacePackages(workspaceRoot) {
187
194
 
188
195
  const config = yaml.parse(content)
189
196
 
190
- return Array.isArray(config?.packages)
191
- ? config.packages.filter(
192
- (pattern) => typeof pattern === 'string' && pattern.length > 0 && !pattern.startsWith('!'),
193
- )
194
- : []
195
- } catch {
196
- return []
197
+ if (config === null || typeof config !== 'object' || Array.isArray(config)) {
198
+ throw new Error('workspace configuration must be a YAML mapping')
199
+ }
200
+
201
+ if (!Object.prototype.hasOwnProperty.call(config, 'packages')) {
202
+ return undefined
203
+ }
204
+
205
+ if (!Array.isArray(config.packages)) {
206
+ throw new Error('packages must be an array')
207
+ }
208
+
209
+ if (config.packages.some((pattern) => typeof pattern !== 'string' || pattern.trim().length === 0)) {
210
+ throw new Error('packages entries must be non-empty strings')
211
+ }
212
+
213
+ return config.packages.map((pattern) => pattern.trim())
214
+ } catch (error) {
215
+ const detail = error instanceof Error ? `: ${error.message}` : ''
216
+ throw new Error(`Failed to read ${path.basename(workspaceFile)}${detail}`)
197
217
  }
198
218
  }
199
219
 
220
+ const WORKSPACE_SCAN_IGNORES = new Set(['.git', 'node_modules'])
221
+
200
222
  function normalizeWorkspacePattern(pattern) {
201
- return pattern.replace(/\*\*?/g, '').replace(/\/$/, '')
223
+ let normalized = pattern.replaceAll('\\', '/').trim()
224
+ if (normalized.startsWith('./')) normalized = normalized.slice(2)
225
+ return normalized.replace(/\/$/, '')
202
226
  }
203
227
 
204
- export function listPackages(workspaceRoot, patterns) {
205
- const packageDirs = []
228
+ function globSource(pattern) {
229
+ let source = ''
206
230
 
207
- const addPackage = (dir) => {
208
- if (isPackageDir(dir)) {
209
- packageDirs.push(dir)
210
- }
211
- }
231
+ for (let index = 0; index < pattern.length; index += 1) {
232
+ const char = pattern[index]
212
233
 
213
- for (const pattern of patterns) {
214
- const base = normalizeWorkspacePattern(pattern)
215
-
216
- const fullPath = path.join(workspaceRoot, base)
234
+ if (char === '*') {
235
+ if (pattern[index + 1] === '*') {
236
+ while (pattern[index + 1] === '*') index += 1
237
+ if (pattern[index + 1] === '/') {
238
+ source += '(?:.*/)?'
239
+ index += 1
240
+ } else {
241
+ source += '.*'
242
+ }
243
+ } else {
244
+ source += '[^/]*'
245
+ }
246
+ continue
247
+ }
217
248
 
218
- if (!exists(fullPath)) {
249
+ if (char === '?') {
250
+ source += '[^/]'
219
251
  continue
220
252
  }
221
253
 
222
- if (pattern.includes('*')) {
223
- const entries = fs.readdirSync(fullPath, {
224
- withFileTypes: true,
225
- })
254
+ if (char === '{') {
255
+ const close = pattern.indexOf('}', index + 1)
256
+ if (close !== -1) {
257
+ const alternatives = pattern
258
+ .slice(index + 1, close)
259
+ .split(',')
260
+ .map((item) => globSource(item))
261
+ source += `(?:${alternatives.join('|')})`
262
+ index = close
263
+ continue
264
+ }
265
+ }
226
266
 
227
- for (const entry of entries) {
228
- if (entry.isDirectory()) {
229
- addPackage(path.join(fullPath, entry.name))
230
- }
267
+ if (char === '[') {
268
+ const close = pattern.indexOf(']', index + 1)
269
+ if (close !== -1) {
270
+ source += pattern.slice(index, close + 1)
271
+ index = close
272
+ continue
231
273
  }
232
- } else {
233
- addPackage(fullPath)
234
274
  }
275
+
276
+ source += char.replace(/[\\.^$+()|]/g, '\\$&')
235
277
  }
236
278
 
279
+ return source
280
+ }
281
+
282
+ function matchesWorkspacePattern(relativePath, pattern) {
283
+ const normalized = normalizeWorkspacePattern(pattern)
284
+ if (normalized === '.') return relativePath === ''
285
+ return new RegExp(`^${globSource(normalized)}$`).test(relativePath)
286
+ }
287
+
288
+ function collectPackageDirs(workspaceRoot) {
289
+ const packageDirs = []
290
+
291
+ const visit = (dir) => {
292
+ if (isPackageDir(dir)) packageDirs.push(dir)
293
+
294
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
295
+ if (!entry.isDirectory() || WORKSPACE_SCAN_IGNORES.has(entry.name)) continue
296
+ visit(path.join(dir, entry.name))
297
+ }
298
+ }
299
+
300
+ visit(workspaceRoot)
237
301
  return packageDirs
238
302
  }
239
303
 
304
+ export function listPackages(workspaceRoot, patterns = undefined) {
305
+ const configuredPatterns = patterns ?? ['**']
306
+ const includePatterns = configuredPatterns.filter((pattern) => !pattern.startsWith('!'))
307
+ const excludePatterns = configuredPatterns
308
+ .filter((pattern) => pattern.startsWith('!'))
309
+ .map((pattern) => pattern.slice(1))
310
+
311
+ if (includePatterns.length === 0) return []
312
+
313
+ const root = path.resolve(workspaceRoot)
314
+ const packageDirs = collectPackageDirs(root)
315
+ const matched = packageDirs.filter((dir) => {
316
+ const relativePath = path.relative(root, dir).replaceAll('\\', '/')
317
+ return (
318
+ includePatterns.some((pattern) => matchesWorkspacePattern(relativePath, pattern)) &&
319
+ !excludePatterns.some((pattern) => matchesWorkspacePattern(relativePath, pattern))
320
+ )
321
+ })
322
+
323
+ return [...new Set(matched)].sort((left, right) => {
324
+ const leftRelative = path.relative(root, left)
325
+ const rightRelative = path.relative(root, right)
326
+ return leftRelative.localeCompare(rightRelative)
327
+ })
328
+ }
329
+
240
330
  function ensureDir(file) {
241
331
  fs.mkdirSync(path.dirname(file), {
242
332
  recursive: true,
@@ -286,6 +376,19 @@ export function mergeEnvFile(templateFile, targetFile) {
286
376
 
287
377
  const targetContent = fs.readFileSync(targetFile, 'utf-8')
288
378
 
379
+ const result = mergeEnvContent(templateContent, targetContent)
380
+
381
+ if (result.type === 'merged') {
382
+ fs.writeFileSync(targetFile, result.content)
383
+ }
384
+
385
+ return {
386
+ type: result.type,
387
+ ...(result.added === undefined ? {} : { added: result.added }),
388
+ }
389
+ }
390
+
391
+ export function mergeEnvContent(templateContent, targetContent) {
289
392
  const templateEnv = parseEnv(templateContent)
290
393
 
291
394
  const targetEnv = parseEnv(targetContent)
@@ -308,10 +411,9 @@ export function mergeEnvFile(templateFile, targetFile) {
308
411
 
309
412
  const nextContent = targetTrimmed ? `${targetTrimmed}\n${appendLines.join('\n')}\n` : `${appendLines.join('\n')}\n`
310
413
 
311
- fs.writeFileSync(targetFile, nextContent)
312
-
313
414
  return {
314
415
  type: 'merged',
315
416
  added: appendLines.length,
417
+ content: nextContent,
316
418
  }
317
419
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opentiny/tiny-robot-cli",
3
- "version": "0.5.2-alpha.3",
3
+ "version": "1.0.0-beta.1",
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": {
@@ -27,10 +28,11 @@
27
28
  },
28
29
  "dependencies": {
29
30
  "@inquirer/prompts": "^8.3.2",
31
+ "@vue/compiler-sfc": "^3.5.40",
30
32
  "commander": "^14.0.3",
31
33
  "picocolors": "^1.1.1",
32
34
  "semver": "^7.8.1",
33
35
  "yaml": "^2.9.0"
34
36
  },
35
- "gitHead": "84475bc4b7695d335abcb249ee5267797b3133d1"
37
+ "gitHead": "49bf2d12171a55e1029bd00bdf2c5dbb53be3eb7"
36
38
  }
@@ -1,2 +1,10 @@
1
+ # Optional custom model endpoints. Leave blank to use official Provider defaults.
2
+ VITE_QWEN_API_URL=
3
+ VITE_DEEPSEEK_API_URL=
4
+
5
+ # Optional custom MCP endpoint. Leave blank to use the official endpoint.
6
+ VITE_AMAP_MCP_URL=
7
+
8
+ # Optional local direct-connection credentials.
1
9
  VITE_ALIYUN_DASHSCOPE_KEY=
2
10
  VITE_DEEPSEEK_API_KEY=
@@ -1,39 +1,71 @@
1
- # __PROJECT_NAME__
1
+ # **PROJECT_NAME**
2
2
 
3
- TinyRobot AI chat starter built with Vue 3 + Vite.
3
+ This project is generated from the TinyRobot Chat Basic template. It covers conversations, streaming responses, cancellation, Markdown rendering, responsive history, themes, model capabilities, and MCP Server/Tool management.
4
4
 
5
- ## Features
5
+ ## MCP support
6
6
 
7
- - Vue 3 + Vite + TypeScript project scaffold
8
- - TinyRobot chat UI with `TrBubbleList`, `TrSender`, and markdown rendering
9
- - Conversation management via `useConversation`
10
- - Model switch with thinking/search capability toggles
11
- - MCP server picker for add/toggle/delete server usage
12
- - MCP transport support for both `sse` and `streamableHttp`
13
- - Tool calling pipeline through `toolPlugin` + MCP `listTools` / `callTool`
14
- - Theme toggle and responsive layout for desktop/mobile
7
+ The demo declares MCP Servers through `mcpServers`:
15
8
 
16
- ## Setup
9
+ ```ts
10
+ const mcpServers = [
11
+ { id: 'amap-maps', name: '高德地图', baseUrl: 'https://...' },
12
+ {
13
+ id: 'model-context-protocol-mcp',
14
+ name: 'Model Context Protocol MCP',
15
+ baseUrl: '/modelcontextprotocol-mcp',
16
+ installed: true,
17
+ },
18
+ ]
19
+ ```
17
20
 
18
- 1. Copy environment variables:
21
+ `installed: true` means initially installed only. The Server remains disabled and does not connect or discover Tools until enabled.
19
22
 
20
- ```bash
21
- cp .env.example .env
22
- ```
23
+ | Server | Transport | Authentication |
24
+ | -------------------------- | --------------- | ---------------- |
25
+ | 高德地图 | Streamable HTTP | Optional API Key |
26
+ | Model Context Protocol MCP | Streamable HTTP | None |
27
+
28
+ Tools are discovered dynamically with `client.listTools()`. Tool choices are stored in the next user message's `runConfig` metadata snapshot, so changing a Tool affects subsequent turns without modifying a request already in progress.
29
+
30
+ Server installation, enabling, Tool loading, retry, de-duplication, and Tool calls are handled by the default adapter in `@opentiny/tiny-robot-chat`. If a Server cannot load its Tools, it remains installed but is automatically disabled. Other enabled Servers and normal chat remain available.
31
+
32
+ ## Model connection modes
33
+
34
+ - API Key only: connects directly to the official Qwen or DeepSeek endpoint.
35
+ - `apiUrl` + API Key: connects to a custom service with frontend authentication.
36
+ - `apiUrl` only: connects to a backend proxy without frontend authentication.
37
+
38
+ The Chat package does not prompt for or validate missing API Keys. The upstream service or backend proxy decides whether authentication is required.
23
39
 
24
- 2. Fill your provider keys in `.env`:
40
+ MCP follows the same three modes: the official endpoint with an optional API Key, a custom `baseUrl` with optional headers, or a proxy `baseUrl` without headers.
41
+
42
+ The basic template already includes the Model Context MCP proxy in `vite.config.ts`. If you use `add chat` in an existing project, the CLI does not modify that project's Vite configuration; add the same `/modelcontextprotocol-mcp` proxy under `server.proxy` manually and restart Vite.
43
+
44
+ ## Model capabilities
45
+
46
+ Qwen models support deep thinking and web search. DeepSeek models support deep thinking. Unsupported capabilities remain visible as disabled buttons so the available model capabilities are clear.
47
+
48
+ DeepSeek models inherit the provider's thinking effort options: `low`, `high`, and `max`, with `high` as the default. The effort selector appears only after `深度思考` is enabled. To customize a model, set `efforts` and `defaultEffort` on that model in `src/App.vue`; use `thinkingRequired: true` only for models that cannot disable thinking.
49
+
50
+ ## Environment variables
51
+
52
+ Copy `.env.example` to `.env.local`. Leave endpoint variables blank for official Provider/MCP defaults, or set them for custom services and backend proxies:
25
53
 
26
54
  ```env
27
- VITE_ALIYUN_DASHSCOPE_KEY=your_dashscope_key
28
- VITE_DEEPSEEK_API_KEY=your_deepseek_key
55
+ VITE_QWEN_API_URL=
56
+ VITE_DEEPSEEK_API_URL=
57
+ VITE_AMAP_MCP_URL=
58
+ VITE_ALIYUN_DASHSCOPE_KEY=
59
+ VITE_DEEPSEEK_API_KEY=
29
60
  ```
30
61
 
31
- `VITE_ALIYUN_DASHSCOPE_KEY` is also used by configured MCP servers that require DashScope authorization.
62
+ For a proxy setup, set for example `VITE_QWEN_API_URL=/api/chat/qwen`, `VITE_DEEPSEEK_API_URL=/api/chat/deepseek`, and `VITE_AMAP_MCP_URL=/api/mcp/amap-maps`, then leave the API Key variables blank.
63
+
64
+ Variables prefixed with `VITE_` are exposed to browser code. Direct browser connections are intended only for servers without secrets and with CORS enabled. Production applications should keep credentials behind a backend or BFF proxy.
32
65
 
33
66
  ## Development
34
67
 
35
68
  ```bash
36
- pnpm install
37
69
  pnpm dev
38
70
  ```
39
71
 
@@ -41,5 +73,4 @@ pnpm dev
41
73
 
42
74
  ```bash
43
75
  pnpm build
44
- pnpm preview
45
76
  ```
@@ -2,9 +2,9 @@
2
2
  <html lang="en">
3
3
  <head>
4
4
  <meta charset="UTF-8" />
5
- <link rel="icon" type="image/x-icon" href="/favicon.ico" />
5
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
- <title>TinyRobot AI Chat</title>
7
+ <title>__PROJECT_NAME__</title>
8
8
  </head>
9
9
  <body>
10
10
  <div id="app"></div>
@@ -1,29 +1,25 @@
1
1
  {
2
2
  "name": "__PROJECT_NAME__",
3
3
  "private": true,
4
- "version": "0.1.0",
4
+ "version": "0.0.0",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "dev": "vite",
8
8
  "build": "vue-tsc -b && vite build",
9
- "preview": "vite preview",
10
- "type-check": "vue-tsc --noEmit"
9
+ "preview": "vite preview"
11
10
  },
12
11
  "dependencies": {
13
- "@modelcontextprotocol/sdk": "^1.18.1",
14
- "@opentiny/tiny-robot": "^0.5.0",
15
- "@opentiny/tiny-robot-kit": "^0.5.0",
16
- "@opentiny/tiny-robot-svgs": "^0.5.0",
17
- "dompurify": "^3.3.1",
18
- "markdown-it": "^14.1.0",
19
- "vue": "^3.5.30"
12
+ "@opentiny/tiny-robot-chat": "__TINY_ROBOT_VERSION__",
13
+ "@opentiny/tiny-robot": "__TINY_ROBOT_VERSION__",
14
+ "@opentiny/tiny-robot-svgs": "__TINY_ROBOT_VERSION__",
15
+ "vue": "^3.5.40"
20
16
  },
21
17
  "devDependencies": {
22
- "@types/node": "^24.12.0",
23
- "@vitejs/plugin-vue": "^6.0.5",
24
- "@vue/tsconfig": "^0.9.0",
25
- "typescript": "~5.9.3",
26
- "vite": "^8.0.1",
27
- "vue-tsc": "^3.2.5"
18
+ "@types/node": "^24.13.3",
19
+ "@vitejs/plugin-vue": "^6.0.8",
20
+ "@vue/tsconfig": "^0.9.1",
21
+ "typescript": "~6.0.2",
22
+ "vite": "^8.2.0",
23
+ "vue-tsc": "^3.3.8"
28
24
  }
29
25
  }
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="48" height="46" fill="none" viewBox="0 0 48 46"><path fill="#863bff" d="M25.946 44.938c-.664.845-2.021.375-2.021-.698V33.937a2.26 2.26 0 0 0-2.262-2.262H10.287c-.92 0-1.456-1.04-.92-1.788l7.48-10.471c1.07-1.497 0-3.578-1.842-3.578H1.237c-.92 0-1.456-1.04-.92-1.788L10.013.474c.214-.297.556-.474.92-.474h28.894c.92 0 1.456 1.04.92 1.788l-7.48 10.471c-1.07 1.498 0 3.579 1.842 3.579h11.377c.943 0 1.473 1.088.89 1.83L25.947 44.94z" style="fill:#863bff;fill:color(display-p3 .5252 .23 1);fill-opacity:1"/><mask id="a" width="48" height="46" x="0" y="0" maskUnits="userSpaceOnUse" style="mask-type:alpha"><path fill="#000" d="M25.842 44.938c-.664.844-2.021.375-2.021-.698V33.937a2.26 2.26 0 0 0-2.262-2.262H10.183c-.92 0-1.456-1.04-.92-1.788l7.48-10.471c1.07-1.498 0-3.579-1.842-3.579H1.133c-.92 0-1.456-1.04-.92-1.787L9.91.473c.214-.297.556-.474.92-.474h28.894c.92 0 1.456 1.04.92 1.788l-7.48 10.471c-1.07 1.498 0 3.578 1.842 3.578h11.377c.943 0 1.473 1.088.89 1.832L25.843 44.94z" style="fill:#000;fill-opacity:1"/></mask><g mask="url(#a)"><g filter="url(#b)"><ellipse cx="5.508" cy="14.704" fill="#ede6ff" rx="5.508" ry="14.704" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -4.47 31.516)"/></g><g filter="url(#c)"><ellipse cx="10.399" cy="29.851" fill="#ede6ff" rx="10.399" ry="29.851" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -39.328 7.883)"/></g><g filter="url(#d)"><ellipse cx="5.508" cy="30.487" fill="#7e14ff" rx="5.508" ry="30.487" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.814 -25.913 -14.639)scale(1 -1)"/></g><g filter="url(#e)"><ellipse cx="5.508" cy="30.599" fill="#7e14ff" rx="5.508" ry="30.599" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.814 -32.644 -3.334)scale(1 -1)"/></g><g filter="url(#f)"><ellipse cx="5.508" cy="30.599" fill="#7e14ff" rx="5.508" ry="30.599" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -34.34 30.47)"/></g><g filter="url(#g)"><ellipse cx="14.072" cy="22.078" fill="#ede6ff" rx="14.072" ry="22.078" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="rotate(93.35 24.506 48.493)scale(-1 1)"/></g><g filter="url(#h)"><ellipse cx="3.47" cy="21.501" fill="#7e14ff" rx="3.47" ry="21.501" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.009 28.708 47.59)scale(-1 1)"/></g><g filter="url(#i)"><ellipse cx="3.47" cy="21.501" fill="#7e14ff" rx="3.47" ry="21.501" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.009 28.708 47.59)scale(-1 1)"/></g><g filter="url(#j)"><ellipse cx=".387" cy="8.972" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(39.51 .387 8.972)"/></g><g filter="url(#k)"><ellipse cx="47.523" cy="-6.092" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 47.523 -6.092)"/></g><g filter="url(#l)"><ellipse cx="41.412" cy="6.333" fill="#47bfff" rx="5.971" ry="9.665" style="fill:#47bfff;fill:color(display-p3 .2799 .748 1);fill-opacity:1" transform="rotate(37.892 41.412 6.333)"/></g><g filter="url(#m)"><ellipse cx="-1.879" cy="38.332" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 -1.88 38.332)"/></g><g filter="url(#n)"><ellipse cx="-1.879" cy="38.332" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 -1.88 38.332)"/></g><g filter="url(#o)"><ellipse cx="35.651" cy="29.907" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 35.651 29.907)"/></g><g filter="url(#p)"><ellipse cx="38.418" cy="32.4" fill="#47bfff" rx="5.971" ry="15.297" style="fill:#47bfff;fill:color(display-p3 .2799 .748 1);fill-opacity:1" transform="rotate(37.892 38.418 32.4)"/></g></g><defs><filter id="b" width="60.045" height="41.654" x="-19.77" y="16.149" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="7.659"/></filter><filter id="c" width="90.34" height="51.437" x="-54.613" y="-7.533" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="7.659"/></filter><filter id="d" width="79.355" height="29.4" x="-49.64" y="2.03" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="e" width="79.579" height="29.4" x="-45.045" y="20.029" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="f" width="79.579" height="29.4" x="-43.513" y="21.178" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="g" width="74.749" height="58.852" x="15.756" y="-17.901" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="7.659"/></filter><filter id="h" width="61.377" height="25.362" x="23.548" y="2.284" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="i" width="61.377" height="25.362" x="23.548" y="2.284" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="j" width="56.045" height="63.649" x="-27.636" y="-22.853" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="k" width="54.814" height="64.646" x="20.116" y="-38.415" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="l" width="33.541" height="35.313" x="24.641" y="-11.323" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="m" width="54.814" height="64.646" x="-29.286" y="6.009" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="n" width="54.814" height="64.646" x="-29.286" y="6.009" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="o" width="54.814" height="64.646" x="8.244" y="-2.416" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="p" width="39.409" height="43.623" x="18.713" y="10.588" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter></defs></svg>
@@ -0,0 +1,24 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg">
2
+ <symbol id="bluesky-icon" viewBox="0 0 16 17">
3
+ <g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
4
+ <defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
5
+ </symbol>
6
+ <symbol id="discord-icon" viewBox="0 0 20 19">
7
+ <path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
8
+ </symbol>
9
+ <symbol id="documentation-icon" viewBox="0 0 21 20">
10
+ <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
11
+ <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
12
+ <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
13
+ </symbol>
14
+ <symbol id="github-icon" viewBox="0 0 19 19">
15
+ <path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
16
+ </symbol>
17
+ <symbol id="social-icon" viewBox="0 0 20 20">
18
+ <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
19
+ <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
20
+ </symbol>
21
+ <symbol id="x-icon" viewBox="0 0 19 19">
22
+ <path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
23
+ </symbol>
24
+ </svg>