@kmlckj/licos-ai-cli 0.0.36 → 0.0.39

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.
@@ -4,7 +4,7 @@ version = "0.1.0"
4
4
  description = "LICOS LangGraph agent project"
5
5
  requires-python = ">=3.12"
6
6
  dependencies = [
7
- "licos-agent-runtime>=0.2.10",
7
+ "licos-agent-runtime>=0.2.11",
8
8
  ]
9
9
 
10
10
  [tool.uv]
@@ -1 +1 @@
1
- licos-agent-runtime>=0.2.10
1
+ licos-agent-runtime>=0.2.11
@@ -25,6 +25,7 @@
25
25
  | ├── src/
26
26
  │ │ └── index.ts # 服务端入口文件
27
27
  | └── package.json # 服务端 package.json
28
+ ├── assets/ # Web 部署可通过 /assets/* 访问的静态资源
28
29
  ├── package.json
29
30
  ├── .licosproj # 预置脚手架脚本(禁止修改)
30
31
  └── .licos # 配置文件(禁止修改)
@@ -238,6 +239,11 @@ cd server && pnpm add --registry=https://registry.npmmirror.com axios cors
238
239
 
239
240
  ## Expo 开发规范
240
241
 
242
+ ### 静态资源
243
+
244
+ - `client/assets/` 用于 Expo/React Native import 和 bundle 资源。
245
+ - 项目根 `assets/` 用于 Web 部署时按 URL 访问的公开静态资源,服务端会映射到 `/assets/*`;H5 页面应优先用相对路径 `assets/name.ext`,避免部署子路径资源 404。开发预览中的跨端资源仍优先通过 `client/assets/` import。
246
+
241
247
  ### 路径别名
242
248
 
243
249
  Expo 配置了 `@/` 路径别名指向 `client/` 目录:
@@ -16,6 +16,13 @@ app.get('/api/v1/health', (req, res) => {
16
16
  res.status(200).json({ status: 'ok' });
17
17
  });
18
18
 
19
+ const projectRoot =
20
+ path.basename(process.cwd()) === "server" ? path.resolve(process.cwd(), "..") : process.cwd();
21
+ const assetsDir = path.resolve(projectRoot, "assets");
22
+ if (fs.existsSync(assetsDir)) {
23
+ app.use("/assets", express.static(assetsDir));
24
+ }
25
+
19
26
  const publicDir = path.resolve(process.cwd(), "public");
20
27
  if (fs.existsSync(publicDir)) {
21
28
  app.use(express.static(publicDir));
@@ -12,6 +12,7 @@
12
12
 
13
13
  ```
14
14
  ├── public/ # 静态资源
15
+ ├── assets/ # 公开静态资源,自定义服务端映射到 /assets/*
15
16
  ├── scripts/ # 构建与启动脚本
16
17
  │ ├── build.sh # 构建脚本
17
18
  │ ├── dev.sh # 开发环境启动脚本
@@ -44,6 +45,11 @@
44
45
 
45
46
  ## 开发规范
46
47
 
48
+ ### 静态资源
49
+
50
+ - 项目根 `assets/` 可放需要按 URL 访问的静态资源,自定义服务端会映射到 `/assets/*`。
51
+ - 部署在子路径时不要硬编码 `/assets/...`,应结合 `basePath` 或相对路径生成可访问 URL。
52
+
47
53
  ### Hydration 问题防范
48
54
 
49
55
  1. 严禁在 JSX 渲染逻辑中直接使用 typeof window、Date.now()、Math.random() 等动态数据。**必须使用 'use client' 并配合 useEffect + useState 确保动态内容仅在客户端挂载后渲染**;同时严禁非法 HTML 嵌套(如 <p> 嵌套 <div>)。
@@ -1,4 +1,6 @@
1
- import { createServer } from 'http';
1
+ import { createServer, type ServerResponse } from 'http';
2
+ import { createReadStream, existsSync, statSync } from 'fs';
3
+ import { extname, resolve, sep } from 'path';
2
4
  import { parse } from 'url';
3
5
  import next from 'next';
4
6
 
@@ -10,6 +12,90 @@ function isProdProjectEnv(value?: string): boolean {
10
12
  const dev = !isProdProjectEnv(process.env.LICOS_PROJECT_ENV);
11
13
  const hostname = process.env.HOSTNAME || 'localhost';
12
14
  const port = parseInt(process.env.PORT || '<%= port %>', 10);
15
+ const projectAssetsDir = resolve(process.cwd(), 'assets');
16
+
17
+ function normalizeBasePath(value?: string): string | undefined {
18
+ const trimmed = value?.trim();
19
+ if (!trimmed || trimmed === '/') return undefined;
20
+ return `/${trimmed.replace(/^\/+|\/+$/g, '')}`;
21
+ }
22
+
23
+ const basePath = normalizeBasePath(process.env.BASE_PATH);
24
+
25
+ function getContentType(filePath: string): string {
26
+ const ext = extname(filePath).toLowerCase();
27
+ const types: Record<string, string> = {
28
+ '.avif': 'image/avif',
29
+ '.css': 'text/css; charset=utf-8',
30
+ '.gif': 'image/gif',
31
+ '.html': 'text/html; charset=utf-8',
32
+ '.ico': 'image/x-icon',
33
+ '.jpg': 'image/jpeg',
34
+ '.jpeg': 'image/jpeg',
35
+ '.js': 'text/javascript; charset=utf-8',
36
+ '.json': 'application/json; charset=utf-8',
37
+ '.map': 'application/json; charset=utf-8',
38
+ '.mp3': 'audio/mpeg',
39
+ '.mp4': 'video/mp4',
40
+ '.otf': 'font/otf',
41
+ '.png': 'image/png',
42
+ '.svg': 'image/svg+xml',
43
+ '.ttf': 'font/ttf',
44
+ '.txt': 'text/plain; charset=utf-8',
45
+ '.wasm': 'application/wasm',
46
+ '.webm': 'video/webm',
47
+ '.webp': 'image/webp',
48
+ '.woff': 'font/woff',
49
+ '.woff2': 'font/woff2',
50
+ '.xml': 'application/xml; charset=utf-8',
51
+ };
52
+ return types[ext] || 'application/octet-stream';
53
+ }
54
+
55
+ function normalizeAssetPath(pathname: string): string | null {
56
+ if (pathname.startsWith('/assets/')) {
57
+ return pathname;
58
+ }
59
+ if (basePath && pathname.startsWith(`${basePath}/assets/`)) {
60
+ return pathname.slice(basePath.length);
61
+ }
62
+ return null;
63
+ }
64
+
65
+ function serveProjectAsset(pathname: string, res: ServerResponse): boolean {
66
+ const assetPath = normalizeAssetPath(pathname);
67
+ if (!assetPath || !existsSync(projectAssetsDir)) {
68
+ return false;
69
+ }
70
+
71
+ let decodedPath: string;
72
+ try {
73
+ decodedPath = decodeURIComponent(assetPath.replace(/^\/assets\/+/, ''));
74
+ } catch {
75
+ return false;
76
+ }
77
+
78
+ const filePath = resolve(projectAssetsDir, decodedPath);
79
+ if (filePath !== projectAssetsDir && !filePath.startsWith(`${projectAssetsDir}${sep}`)) {
80
+ res.statusCode = 403;
81
+ res.end('Forbidden');
82
+ return true;
83
+ }
84
+
85
+ try {
86
+ const stat = statSync(filePath);
87
+ if (!stat.isFile()) {
88
+ return false;
89
+ }
90
+ res.statusCode = 200;
91
+ res.setHeader('Content-Type', getContentType(filePath));
92
+ res.setHeader('Content-Length', stat.size);
93
+ createReadStream(filePath).pipe(res);
94
+ return true;
95
+ } catch {
96
+ return false;
97
+ }
98
+ }
13
99
 
14
100
  // Create Next.js app
15
101
  const app = next({ dev, hostname, port });
@@ -19,6 +105,9 @@ app.prepare().then(() => {
19
105
  const server = createServer(async (req, res) => {
20
106
  try {
21
107
  const parsedUrl = parse(req.url!, true);
108
+ if (serveProjectAsset(parsedUrl.pathname || '', res)) {
109
+ return;
110
+ }
22
111
  await handle(req, res, parsedUrl);
23
112
  } catch (err) {
24
113
  console.error('Error occurred handling', req.url, err);
@@ -42,3 +42,4 @@
42
42
  ## 开发规范
43
43
 
44
44
  - 使用 Tailwind CSS 进行样式开发
45
+ - 项目根 `assets/` 会通过 Nitro 映射到 `/assets/*`,需要按 URL 访问时使用 `useRuntimeConfig().public.baseURL` 派生路径,例如 `assetUrl('assets/logo.svg')`;业务 CSS 仍优先放到 `nuxt.config.ts` 的 `css` 数组或组件 `<style scoped>`。
@@ -145,6 +145,12 @@ export default defineNuxtConfig({
145
145
 
146
146
  // Security headers (Content Security Policy)
147
147
  nitro: {
148
+ publicAssets: [
149
+ {
150
+ baseURL: '/assets',
151
+ dir: resolve(__dirname, 'assets'),
152
+ },
153
+ ],
148
154
  routeRules: {
149
155
  '/**': {
150
156
  headers: {
@@ -20,9 +20,15 @@ pnpm remove <package> # 移除依赖
20
20
  - `src/` 下禁止直接使用 `process.env`,不要使用 `__PUBLIC_PATH__`。请求前缀使用全局 `PROJECT_DOMAIN`,构建平台判断使用全局 `LICOS_TARO_ENV`。
21
21
  - 不要在业务代码里使用裸标识符 `TARO_ENV`。如需展示文字 “TARO_ENV”,只作为普通中文/英文说明文本使用,不要把它当作变量读取。
22
22
  - Taro API 只允许单次导入,统一写 `import Taro, { useLoad } from '@tarojs/taro'`,不要同时写 `import { useLoad } from '@tarojs/taro'` 和 `import Taro from '@tarojs/taro'`。
23
+ - React hooks 必须从 `react` 导入,例如 `import { useMemo, useState } from 'react'`;不要从 `@tarojs/taro` 导入 `useState`、`useEffect`、`useMemo`。
24
+ - 不要重复导入同一个包。`@tarojs/taro`、`@tarojs/components`、`react`、`lucide-react-taro` 每个文件最多一个 import 语句。
23
25
  - 需要在 `src/` 展示构建环境或项目域名时,直接读取全局常量:`LICOS_TARO_ENV || 'unknown'`、`PROJECT_DOMAIN || ''`,不要写 `process.env.TARO_ENV` 或 `process.env.PROJECT_DOMAIN`。
24
26
  - 需要引用本地资源时使用 ESM import,例如 `import logoUrl from '@/assets/images/logo.svg'`,不要在 `src/` 中写 CommonJS `require(...)`。
27
+ - 根目录 `assets/` 是 H5 公开静态目录,不是 `@/assets` 源码别名。放在根 `assets/images/foo.svg` 的文件必须用相对 URL `assets/images/foo.svg`,不要写 `import foo from '@/assets/images/foo.svg'`。只有文件真实存在于 `src/assets/...` 时,才允许从 `@/assets/...` 导入。
28
+ - 不要使用浏览器 DOM 全局做资源探测。禁止 `window`、`document`、`new Image()`、`window.Image`、`globalThis.Image`;图片状态通过 Taro `<Image onLoad onError />` 或直接渲染处理。
25
29
  - 从第三方图标包导入前必须确认导出存在;不确定时优先使用已验证的 `Package`、`Globe`、`Cpu`、`Layers` 等基础图标,避免引入不存在的命名导出导致构建失败。
30
+ - 禁止输出不完整模板字符串、占位引号或非法 TS 字符串,例如 `"""`、未闭合的引号、未转义的英文单引号。文案中包含引号时使用反引号或正确转义。
31
+ - 创建或测试小程序时,必须替换默认 `src/pages/index/index.tsx` 为实际首页,或把 `src/app.config.ts` 的第一个页面改为实际入口;不要新增测试页后保留默认占位首页。
26
32
 
27
33
  正确示例:
28
34
 
@@ -53,6 +59,8 @@ export default function IndexPage() {
53
59
 
54
60
  ## 图片与视频资源使用规范
55
61
 
62
+ 项目根 `assets/` 是 H5 预览/部署的公开静态资源目录,服务端映射到 `/assets/*`。H5 页面按 URL 引用少量静态文件时优先使用相对路径 `assets/name.ext`,避免部署在 `/p/{projectId}/` 子路径时资源 404;大图片、视频和用户上传内容仍走对象存储。不要把根 `assets/` 文件当作 `src/assets` 导入。
63
+
56
64
  **强制规则**:图片、视频等静态资源必须通过 TOS 对象存储管理,代码中使用 TOS 返回的 URL 引用。
57
65
 
58
66
  1. **资源存储方式**:
@@ -205,7 +213,7 @@ CRITICAL:
205
213
  - 颜色、间距、圆角、边框、阴影、排版、flex/grid、宽高等常规样式必须收敛在 Tailwind 类名中。
206
214
  - 禁止使用 `w-[340px]`、`text-[14px]`、`p-[16px]` 这类带 `px` 的 Tailwind 任意值。
207
215
  - 禁止使用颜色不透明度简写,例如 `bg-primary/10`、`text-slate-500/70`、`border-white/20`;小程序端 opacity 容易丢失,必须拆成基础颜色类和 `bg-opacity-*` / `text-opacity-*` / `border-opacity-*`。
208
- - 禁止使用小数值类名,例如 `space-y-1.5`、`w-0.5`、`h-2.5`;统一改成整数刻度,例如 `space-y-2`、`w-1`、`h-3`。
216
+ - 禁止使用小数值或斜杠分数类名,例如 `space-y-1.5`、`w-0.5`、`h-2.5`、`mt-0.5`、`h-3/5`;统一改成整数刻度,例如 `space-y-2`、`w-1`、`h-3`、`mt-1`。
209
217
  - 禁止使用 `peer-*`、`group-*`、`has-*`、复杂任意选择器和属性选择器类名;这些写法在小程序构建或上传时容易失败。
210
218
  - 禁止使用 `style={{ width: '200px' }}`、`fontSize: '14px'` 这类硬编码尺寸。
211
219
  - Taro 会通过 `pxtransform` 将尺寸转换为跨端单位(小程序 `rpx`、H5 `rem`),业务代码里直接写 `px` 容易导致不同端显示不一致。
@@ -12,6 +12,66 @@ import pkg from '../package.json';
12
12
 
13
13
  dotenv.config({ path: path.resolve(__dirname, '../.env.local') });
14
14
 
15
+ const projectAssetsDir = path.resolve(__dirname, '../assets');
16
+ const assetMimeTypes: Record<string, string> = {
17
+ '.avif': 'image/avif',
18
+ '.css': 'text/css; charset=utf-8',
19
+ '.gif': 'image/gif',
20
+ '.html': 'text/html; charset=utf-8',
21
+ '.ico': 'image/x-icon',
22
+ '.jpg': 'image/jpeg',
23
+ '.jpeg': 'image/jpeg',
24
+ '.js': 'text/javascript; charset=utf-8',
25
+ '.json': 'application/json; charset=utf-8',
26
+ '.mp3': 'audio/mpeg',
27
+ '.mp4': 'video/mp4',
28
+ '.otf': 'font/otf',
29
+ '.png': 'image/png',
30
+ '.svg': 'image/svg+xml',
31
+ '.ttf': 'font/ttf',
32
+ '.txt': 'text/plain; charset=utf-8',
33
+ '.wasm': 'application/wasm',
34
+ '.webm': 'video/webm',
35
+ '.webp': 'image/webp',
36
+ '.woff': 'font/woff',
37
+ '.woff2': 'font/woff2',
38
+ };
39
+
40
+ const createProjectAssetsStaticPlugin = () => ({
41
+ name: 'licos-project-assets-static',
42
+ configureServer(server) {
43
+ server.middlewares.use('/assets', (req, res, next) => {
44
+ if (!fs.existsSync(projectAssetsDir)) {
45
+ return next();
46
+ }
47
+ const requestPath = (req.url || '')
48
+ .split('?')[0]
49
+ .replace(/^\/assets\/?/, '')
50
+ .replace(/^\/+/, '');
51
+ let decodedPath = '';
52
+ try {
53
+ decodedPath = decodeURIComponent(requestPath);
54
+ } catch {
55
+ return next();
56
+ }
57
+ const filePath = path.resolve(projectAssetsDir, decodedPath);
58
+ if (filePath !== projectAssetsDir && !filePath.startsWith(`${projectAssetsDir}${path.sep}`)) {
59
+ res.statusCode = 403;
60
+ res.end('Forbidden');
61
+ return;
62
+ }
63
+ if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
64
+ return next();
65
+ }
66
+ res.setHeader(
67
+ 'Content-Type',
68
+ assetMimeTypes[path.extname(filePath).toLowerCase()] || 'application/octet-stream',
69
+ );
70
+ fs.createReadStream(filePath).pipe(res);
71
+ });
72
+ },
73
+ });
74
+
15
75
  const generateTTProjectConfig = (outputRoot: string) => {
16
76
  const config = {
17
77
  miniprogramRoot: './',
@@ -121,6 +181,7 @@ export default defineConfig<'vite'>(async (merge, _env) => {
121
181
  }
122
182
  },
123
183
  },
184
+ ...(isH5 ? [createProjectAssetsStaticPlugin()] : []),
124
185
  {
125
186
  name: 'hmr-config-plugin',
126
187
  config() {
@@ -28,6 +28,12 @@ async function bootstrap() {
28
28
  app.use(express.json({ limit: '50mb' }));
29
29
  app.use(express.urlencoded({ limit: '50mb', extended: true }));
30
30
 
31
+ const projectRoot = path.resolve(__dirname, '../..');
32
+ const assetsDir = path.join(projectRoot, 'assets');
33
+ if (fs.existsSync(assetsDir)) {
34
+ app.use('/assets', express.static(assetsDir));
35
+ }
36
+
31
37
  const staticDir = path.resolve(__dirname, '../../dist-web');
32
38
  const indexHtml = path.join(staticDir, 'index.html');
33
39
  if (fs.existsSync(indexHtml)) {
@@ -21,6 +21,7 @@
21
21
  │ ├── index.css # 全局样式
22
22
  │ ├── index.ts # 客户端入口
23
23
  │ └── main.ts # 主逻辑
24
+ ├── assets/ # 公开静态资源,运行时映射到 /assets/*
24
25
  ├── index.html # 入口 HTML
25
26
  ├── package.json # 项目依赖管理
26
27
  ├── tsconfig.json # TypeScript 配置
@@ -41,3 +42,4 @@
41
42
  ## 开发规范
42
43
 
43
44
  - 使用 Tailwind CSS 进行样式开发
45
+ - 项目根 `assets/` 可放需要按 URL 访问的静态资源;部署在子路径时必须通过 `import.meta.env.BASE_URL` 派生访问路径,例如 `assetUrl('assets/logo.svg')`。
@@ -14,5 +14,12 @@ pnpm vite build
14
14
 
15
15
  echo "Bundling server with tsup..."
16
16
  pnpm tsup server/server.ts --format cjs --platform node --target node20 --outDir dist-server --no-splitting --no-minify --external vite
17
+ if [ -f dist-server/server.cjs ] && [ ! -f dist-server/server.js ]; then
18
+ mv dist-server/server.cjs dist-server/server.js
19
+ fi
20
+ if [ ! -f dist-server/server.js ]; then
21
+ echo "Build failed: dist-server/server.js was not generated"
22
+ exit 1
23
+ fi
17
24
 
18
25
  echo "Build completed successfully!"
@@ -14,7 +14,15 @@ DEPLOY_RUN_PORT="${DEPLOY_RUN_PORT:-$PORT}"
14
14
  start_service() {
15
15
  cd "${PROJECT_DIR}"
16
16
  echo "Starting express production server on port ${DEPLOY_RUN_PORT}..."
17
- PORT=$DEPLOY_RUN_PORT node dist-server/server.js
17
+ SERVER_ENTRY="dist-server/server.js"
18
+ if [ ! -f "$SERVER_ENTRY" ] && [ -f "dist-server/server.cjs" ]; then
19
+ SERVER_ENTRY="dist-server/server.cjs"
20
+ fi
21
+ if [ ! -f "$SERVER_ENTRY" ]; then
22
+ echo "Server entry not found: dist-server/server.js"
23
+ exit 1
24
+ fi
25
+ PORT=$DEPLOY_RUN_PORT node "$SERVER_ENTRY"
18
26
  }
19
27
 
20
28
  echo "Starting express production server on port ${DEPLOY_RUN_PORT}..."
@@ -13,6 +13,13 @@ function isProdProjectEnv(value?: string): boolean {
13
13
 
14
14
  const isDev = !isProdProjectEnv(process.env.LICOS_PROJECT_ENV);
15
15
 
16
+ function mountProjectAssets(app: Application) {
17
+ const assetsPath = path.resolve(process.cwd(), 'assets');
18
+ if (fs.existsSync(assetsPath)) {
19
+ app.use('/assets', express.static(assetsPath));
20
+ }
21
+ }
22
+
16
23
  /**
17
24
  * 集成 Vite 开发服务器(中间件模式)
18
25
  */
@@ -31,6 +38,8 @@ export async function setupViteMiddleware(app: Application) {
31
38
  appType: 'spa',
32
39
  });
33
40
 
41
+ mountProjectAssets(app);
42
+
34
43
  // 使用 Vite middleware
35
44
  app.use(vite.middlewares);
36
45
 
@@ -48,7 +57,8 @@ export function setupStaticServer(app: Application) {
48
57
  process.exit(1);
49
58
  }
50
59
 
51
- // 1. 服务静态文件(如果存在对应文件则直接返回)
60
+ // 1. 服务项目根 assets/ 和构建产物静态文件(如果存在对应文件则直接返回)
61
+ mountProjectAssets(app);
52
62
  app.use(express.static(distPath));
53
63
 
54
64
  // 2. SPA fallback - 所有未处理的请求返回 index.html
@@ -4,7 +4,7 @@ version = "0.1.0"
4
4
  description = "LICOS LangGraph workflow project"
5
5
  requires-python = ">=3.12"
6
6
  dependencies = [
7
- "licos-agent-runtime>=0.2.10",
7
+ "licos-agent-runtime>=0.2.11",
8
8
  ]
9
9
 
10
10
  [tool.uv]
@@ -1 +1 @@
1
- licos-agent-runtime>=0.2.10
1
+ licos-agent-runtime>=0.2.11
package/lib/cli.js CHANGED
@@ -2109,7 +2109,7 @@ const EventBuilder = {
2109
2109
  };
2110
2110
 
2111
2111
  var name = "@kmlckj/licos-ai-cli";
2112
- var version = "0.0.36";
2112
+ var version = "0.0.39";
2113
2113
  var description = "LICOS AI coding workspace CLI - project template engine and dev tools";
2114
2114
  var license = "MIT";
2115
2115
  var author = "kmlckj";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kmlckj/licos-ai-cli",
3
- "version": "0.0.36",
3
+ "version": "0.0.39",
4
4
  "description": "LICOS AI coding workspace CLI - project template engine and dev tools",
5
5
  "license": "MIT",
6
6
  "author": "kmlckj",