@coze-arch/cli 0.0.1-beta.5
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 +142 -0
- package/bin/main +2 -0
- package/lib/__templates__/expo/.coze +7 -0
- package/lib/__templates__/expo/.cozeproj/scripts/deploy_build.sh +109 -0
- package/lib/__templates__/expo/.cozeproj/scripts/deploy_run.sh +257 -0
- package/lib/__templates__/expo/README.md +13 -0
- package/lib/__templates__/expo/_gitignore +11 -0
- package/lib/__templates__/expo/app.json +63 -0
- package/lib/__templates__/expo/babel.config.js +9 -0
- package/lib/__templates__/expo/client/app/(tabs)/_layout.tsx +43 -0
- package/lib/__templates__/expo/client/app/(tabs)/home.tsx +1 -0
- package/lib/__templates__/expo/client/app/(tabs)/index.tsx +7 -0
- package/lib/__templates__/expo/client/app/+not-found.tsx +79 -0
- package/lib/__templates__/expo/client/app/_layout.tsx +33 -0
- package/lib/__templates__/expo/client/assets/fonts/SpaceMono-Regular.ttf +0 -0
- package/lib/__templates__/expo/client/assets/images/adaptive-icon.png +0 -0
- package/lib/__templates__/expo/client/assets/images/default-avatar.png +0 -0
- package/lib/__templates__/expo/client/assets/images/favicon.png +0 -0
- package/lib/__templates__/expo/client/assets/images/icon.png +0 -0
- package/lib/__templates__/expo/client/assets/images/partial-react-logo.png +0 -0
- package/lib/__templates__/expo/client/assets/images/react-logo.png +0 -0
- package/lib/__templates__/expo/client/assets/images/react-logo@2x.png +0 -0
- package/lib/__templates__/expo/client/assets/images/react-logo@3x.png +0 -0
- package/lib/__templates__/expo/client/assets/images/splash-icon.png +0 -0
- package/lib/__templates__/expo/client/components/Screen.tsx +330 -0
- package/lib/__templates__/expo/client/components/SmartDateInput.tsx +238 -0
- package/lib/__templates__/expo/client/constants/theme.ts +118 -0
- package/lib/__templates__/expo/client/contexts/AuthContext.tsx +142 -0
- package/lib/__templates__/expo/client/hooks/useColorScheme.ts +1 -0
- package/lib/__templates__/expo/client/hooks/useTheme.ts +13 -0
- package/lib/__templates__/expo/client/index.js +11 -0
- package/lib/__templates__/expo/client/screens/home/index.tsx +54 -0
- package/lib/__templates__/expo/client/screens/home/styles.ts +332 -0
- package/lib/__templates__/expo/client/scripts/install-missing-deps.js +80 -0
- package/lib/__templates__/expo/client/utils/index.ts +55 -0
- package/lib/__templates__/expo/eslint-formatter-simple.mjs +49 -0
- package/lib/__templates__/expo/eslint.config.mjs +98 -0
- package/lib/__templates__/expo/metro.config.js +53 -0
- package/lib/__templates__/expo/package.json +100 -0
- package/lib/__templates__/expo/pnpm-lock.yaml +13978 -0
- package/lib/__templates__/expo/src/index.ts +12 -0
- package/lib/__templates__/expo/template.config.js +49 -0
- package/lib/__templates__/expo/tsconfig.json +24 -0
- package/lib/__templates__/nextjs/.coze +11 -0
- package/lib/__templates__/nextjs/.vscode/settings.json +121 -0
- package/lib/__templates__/nextjs/README.md +36 -0
- package/lib/__templates__/nextjs/_gitignore +99 -0
- package/lib/__templates__/nextjs/_npmrc +22 -0
- package/lib/__templates__/nextjs/eslint.config.mjs +18 -0
- package/lib/__templates__/nextjs/next-env.d.ts +6 -0
- package/lib/__templates__/nextjs/next.config.ts +7 -0
- package/lib/__templates__/nextjs/package.json +32 -0
- package/lib/__templates__/nextjs/pnpm-lock.yaml +4061 -0
- package/lib/__templates__/nextjs/postcss.config.mjs +7 -0
- package/lib/__templates__/nextjs/public/file.svg +1 -0
- package/lib/__templates__/nextjs/public/globe.svg +1 -0
- package/lib/__templates__/nextjs/public/next.svg +1 -0
- package/lib/__templates__/nextjs/public/vercel.svg +1 -0
- package/lib/__templates__/nextjs/public/window.svg +1 -0
- package/lib/__templates__/nextjs/scripts/build.sh +14 -0
- package/lib/__templates__/nextjs/scripts/dev.sh +51 -0
- package/lib/__templates__/nextjs/scripts/start.sh +15 -0
- package/lib/__templates__/nextjs/src/app/favicon.ico +0 -0
- package/lib/__templates__/nextjs/src/app/globals.css +26 -0
- package/lib/__templates__/nextjs/src/app/layout.tsx +34 -0
- package/lib/__templates__/nextjs/src/app/page.tsx +66 -0
- package/lib/__templates__/nextjs/template.config.js +55 -0
- package/lib/__templates__/nextjs/tsconfig.json +34 -0
- package/lib/__templates__/react-rsbuild/.coze +11 -0
- package/lib/__templates__/react-rsbuild/.vscode/settings.json +121 -0
- package/lib/__templates__/react-rsbuild/README.md +61 -0
- package/lib/__templates__/react-rsbuild/_gitignore +97 -0
- package/lib/__templates__/react-rsbuild/_npmrc +22 -0
- package/lib/__templates__/react-rsbuild/package.json +31 -0
- package/lib/__templates__/react-rsbuild/pnpm-lock.yaml +997 -0
- package/lib/__templates__/react-rsbuild/rsbuild.config.ts +13 -0
- package/lib/__templates__/react-rsbuild/scripts/build.sh +14 -0
- package/lib/__templates__/react-rsbuild/scripts/dev.sh +51 -0
- package/lib/__templates__/react-rsbuild/scripts/start.sh +15 -0
- package/lib/__templates__/react-rsbuild/src/App.tsx +60 -0
- package/lib/__templates__/react-rsbuild/src/index.css +21 -0
- package/lib/__templates__/react-rsbuild/src/index.html +12 -0
- package/lib/__templates__/react-rsbuild/src/index.tsx +16 -0
- package/lib/__templates__/react-rsbuild/tailwind.config.js +9 -0
- package/lib/__templates__/react-rsbuild/template.config.js +54 -0
- package/lib/__templates__/react-rsbuild/tsconfig.json +17 -0
- package/lib/__templates__/rsbuild/.coze +11 -0
- package/lib/__templates__/rsbuild/.vscode/settings.json +7 -0
- package/lib/__templates__/rsbuild/README.md +61 -0
- package/lib/__templates__/rsbuild/_gitignore +97 -0
- package/lib/__templates__/rsbuild/_npmrc +22 -0
- package/lib/__templates__/rsbuild/package.json +24 -0
- package/lib/__templates__/rsbuild/pnpm-lock.yaml +888 -0
- package/lib/__templates__/rsbuild/rsbuild.config.ts +12 -0
- package/lib/__templates__/rsbuild/scripts/build.sh +14 -0
- package/lib/__templates__/rsbuild/scripts/dev.sh +51 -0
- package/lib/__templates__/rsbuild/scripts/start.sh +15 -0
- package/lib/__templates__/rsbuild/src/index.css +21 -0
- package/lib/__templates__/rsbuild/src/index.html +12 -0
- package/lib/__templates__/rsbuild/src/index.ts +5 -0
- package/lib/__templates__/rsbuild/src/main.ts +65 -0
- package/lib/__templates__/rsbuild/tailwind.config.js +9 -0
- package/lib/__templates__/rsbuild/template.config.js +54 -0
- package/lib/__templates__/rsbuild/tsconfig.json +16 -0
- package/lib/__templates__/templates.json +100 -0
- package/lib/__templates__/vite/.coze +11 -0
- package/lib/__templates__/vite/.vscode/settings.json +7 -0
- package/lib/__templates__/vite/README.md +61 -0
- package/lib/__templates__/vite/_gitignore +66 -0
- package/lib/__templates__/vite/_npmrc +22 -0
- package/lib/__templates__/vite/index.html +13 -0
- package/lib/__templates__/vite/package.json +24 -0
- package/lib/__templates__/vite/pnpm-lock.yaml +1249 -0
- package/lib/__templates__/vite/postcss.config.js +6 -0
- package/lib/__templates__/vite/scripts/build.sh +14 -0
- package/lib/__templates__/vite/scripts/dev.sh +51 -0
- package/lib/__templates__/vite/scripts/start.sh +15 -0
- package/lib/__templates__/vite/src/index.css +21 -0
- package/lib/__templates__/vite/src/index.ts +5 -0
- package/lib/__templates__/vite/src/main.ts +65 -0
- package/lib/__templates__/vite/tailwind.config.js +9 -0
- package/lib/__templates__/vite/template.config.js +55 -0
- package/lib/__templates__/vite/tsconfig.json +16 -0
- package/lib/__templates__/vite/vite.config.ts +15 -0
- package/lib/cli.js +1575 -0
- package/package.json +70 -0
package/README.md
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# @coze-coding/cli
|
|
2
|
+
|
|
3
|
+
Coze Coding 的项目模板引擎,提供基于前端技术栈的项目初始化能力。
|
|
4
|
+
|
|
5
|
+
## 特性
|
|
6
|
+
|
|
7
|
+
- 🚀 **快速初始化**: 一键创建预配置的项目模板
|
|
8
|
+
- 🤖 **AI 优先设计**: 专为 AI Agent 消费设计,提供清晰的错误信息和自描述能力
|
|
9
|
+
- 📦 **多模板支持**: 支持 React、Next.js、HTML 等多种技术栈
|
|
10
|
+
- 🔧 **类型安全**: 基于 JSON Schema 的参数校验和 TypeScript 类型生成
|
|
11
|
+
- ⚡ **命令代理**: 统一的 dev/build/start 命令,抹平不同技术栈差异
|
|
12
|
+
|
|
13
|
+
## 安装
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
rush update
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## 使用
|
|
20
|
+
|
|
21
|
+
### 初始化项目
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
coze-coding init --template react --app-name my-app --use-typescript --use-tailwind
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### 查看可用模板
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
coze-coding init --help
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### 开发命令
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
# 启动开发服务器
|
|
37
|
+
coze-coding dev
|
|
38
|
+
|
|
39
|
+
# 构建生产版本
|
|
40
|
+
coze-coding build
|
|
41
|
+
|
|
42
|
+
# 启动生产服务器
|
|
43
|
+
coze-coding start
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## 模板参数
|
|
47
|
+
|
|
48
|
+
每个模板都有自己的参数配置,通过 CLI 参数透传。参数名使用 kebab-case 格式,会自动转换为 camelCase 传入模板配置。
|
|
49
|
+
|
|
50
|
+
例如:
|
|
51
|
+
- `--app-name` → `appName`
|
|
52
|
+
- `--use-typescript` → `useTypeScript`
|
|
53
|
+
- `--port` → `port`
|
|
54
|
+
|
|
55
|
+
## 开发模板
|
|
56
|
+
|
|
57
|
+
### 模板结构
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
template-name/
|
|
61
|
+
├── template.config.ts # 模板配置(开发时)
|
|
62
|
+
├── template.config.js # 模板配置(生产时)
|
|
63
|
+
├── .coze # 项目配置文件
|
|
64
|
+
└── src/ # 源代码(支持 EJS 模板)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### 参数定义
|
|
68
|
+
|
|
69
|
+
使用 JSON Schema 定义模板参数:
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
import type { JSONSchemaType } from 'ajv';
|
|
73
|
+
|
|
74
|
+
export interface TemplateParams {
|
|
75
|
+
appName: string;
|
|
76
|
+
useTypeScript: boolean;
|
|
77
|
+
port: number;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export const paramsSchema: JSONSchemaType<TemplateParams> = {
|
|
81
|
+
type: 'object',
|
|
82
|
+
properties: {
|
|
83
|
+
appName: {
|
|
84
|
+
type: 'string',
|
|
85
|
+
minLength: 1,
|
|
86
|
+
pattern: '^[a-z0-9-]+$',
|
|
87
|
+
cliParam: '--app-name',
|
|
88
|
+
description: 'Application name'
|
|
89
|
+
},
|
|
90
|
+
useTypeScript: {
|
|
91
|
+
type: 'boolean',
|
|
92
|
+
default: true,
|
|
93
|
+
cliParam: '--use-typescript',
|
|
94
|
+
description: 'Enable TypeScript'
|
|
95
|
+
},
|
|
96
|
+
port: {
|
|
97
|
+
type: 'number',
|
|
98
|
+
default: 3000,
|
|
99
|
+
minimum: 1024,
|
|
100
|
+
maximum: 65535,
|
|
101
|
+
cliParam: '--port',
|
|
102
|
+
description: 'Development server port'
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
required: ['appName'],
|
|
106
|
+
additionalProperties: false
|
|
107
|
+
};
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### 生命周期钩子
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
export const onBeforeRender = (context) => {
|
|
114
|
+
// 渲染前处理
|
|
115
|
+
return context;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
export const onAfterRender = (context, outputPath) => {
|
|
119
|
+
// 渲染后处理
|
|
120
|
+
};
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## 编码规范
|
|
124
|
+
|
|
125
|
+
本项目严格遵循函数式编程风格:
|
|
126
|
+
|
|
127
|
+
- ✅ 使用纯函数和不可变数据
|
|
128
|
+
- ✅ 函数组合和高阶函数
|
|
129
|
+
- ✅ 使用工厂函数和闭包
|
|
130
|
+
- ❌ 禁止使用 class
|
|
131
|
+
- ❌ 避免副作用和可变状态
|
|
132
|
+
|
|
133
|
+
详见 [技术设计文档](./docs/technical-design.md#7-编码规范)
|
|
134
|
+
|
|
135
|
+
## 文档
|
|
136
|
+
|
|
137
|
+
- [需求文档](./docs/requirement.md)
|
|
138
|
+
- [技术设计](./docs/technical-design.md)
|
|
139
|
+
|
|
140
|
+
## 许可证
|
|
141
|
+
|
|
142
|
+
内部项目
|
package/bin/main
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
if [ -z "${BASH_VERSION:-}" ]; then exec /usr/bin/env bash "$0" "$@"; fi
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
ROOT_DIR="$(pwd)"
|
|
5
|
+
LOG_DIR="${COZE_LOG_DIR:-$ROOT_DIR/logs}"
|
|
6
|
+
LOG_FILE="$LOG_DIR/app.log"
|
|
7
|
+
mkdir -p "$LOG_DIR"
|
|
8
|
+
|
|
9
|
+
# ==================== 配置项 ====================
|
|
10
|
+
SERVER_DIR="app"
|
|
11
|
+
EXPO_DIR="expo"
|
|
12
|
+
CHECK_HASH_SCRIPT="$ROOT_DIR/check_hash.py"
|
|
13
|
+
# ==================== 工具函数 ====================
|
|
14
|
+
info() {
|
|
15
|
+
echo -e "\033[32m[INFO] $1\033[0m"
|
|
16
|
+
}
|
|
17
|
+
warn() {
|
|
18
|
+
echo -e "\033[33m[WARN] $1\033[0m"
|
|
19
|
+
}
|
|
20
|
+
error() {
|
|
21
|
+
echo -e "\033[31m[ERROR] $1\033[0m"
|
|
22
|
+
exit 1
|
|
23
|
+
}
|
|
24
|
+
check_command() {
|
|
25
|
+
if ! command -v "$1" &> /dev/null; then
|
|
26
|
+
error "命令 $1 未找到,请先安装"
|
|
27
|
+
fi
|
|
28
|
+
}
|
|
29
|
+
write_log() {
|
|
30
|
+
local level="${1:-INFO}"
|
|
31
|
+
local msg="${2:-}"
|
|
32
|
+
python - "$LOG_FILE" "$level" "$msg" <<'PY'
|
|
33
|
+
import os,sys,json,time
|
|
34
|
+
from datetime import datetime, timezone, timedelta
|
|
35
|
+
path, level, msg = sys.argv[1], sys.argv[2].upper(), sys.argv[3]
|
|
36
|
+
ts = int(time.time()*1000)
|
|
37
|
+
tz = timezone(timedelta(hours=8))
|
|
38
|
+
dt_str = datetime.now(tz).strftime('%Y-%m-%d %H:%M:%S')
|
|
39
|
+
msg = msg.replace('\\n', '\n')
|
|
40
|
+
msg = f"{dt_str} [{level}] {msg}"
|
|
41
|
+
with open(path,'a',encoding='utf-8',buffering=1) as f:
|
|
42
|
+
f.write(json.dumps({"level":level,"message":msg,"timestamp":ts},ensure_ascii=False)+'\n')
|
|
43
|
+
f.flush(); os.fsync(f.fileno())
|
|
44
|
+
PY
|
|
45
|
+
|
|
46
|
+
case "$level" in
|
|
47
|
+
INFO) info "$msg" ;;
|
|
48
|
+
WARN) warn "$msg" ;;
|
|
49
|
+
ERROR) echo -e "\033[31m[ERROR] $msg\033[0m" ;;
|
|
50
|
+
*) info "$msg" ;;
|
|
51
|
+
esac
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
# ==================== 前置检查 ====================
|
|
55
|
+
write_log "INFO" "==================== 开始构建 ===================="
|
|
56
|
+
|
|
57
|
+
write_log "INFO" "检查根目录 pre_install.py"
|
|
58
|
+
if [ -f "$ROOT_DIR/pre_install.py" ]; then
|
|
59
|
+
write_log "INFO" "执行:python $ROOT_DIR/pre_install.py"
|
|
60
|
+
python "$ROOT_DIR/pre_install.py" || write_log "ERROR" "pre_install.py 执行失败"
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
write_log "INFO" "开始执行构建脚本(build_dev.sh)..."
|
|
64
|
+
write_log "INFO" "正在检查依赖命令是否存在..."
|
|
65
|
+
# 检查核心命令
|
|
66
|
+
check_command "pip"
|
|
67
|
+
check_command "python"
|
|
68
|
+
check_command "pnpm"
|
|
69
|
+
check_command "npm"
|
|
70
|
+
|
|
71
|
+
# ==================== 步骤 1:安装项目依赖 ====================
|
|
72
|
+
write_log "INFO" "==================== 安装项目依赖 ===================="
|
|
73
|
+
if [ ! -f "package.json" ]; then
|
|
74
|
+
write_log "ERROR" "项目目录下无 package.json,不是合法的 Node.js 项目"
|
|
75
|
+
fi
|
|
76
|
+
# 步骤 2.1/2.2:安装 Expo 项目依赖(按哈希变化执行)
|
|
77
|
+
if [ -f "$CHECK_HASH_SCRIPT" ]; then
|
|
78
|
+
write_log "INFO" "检测 package.json 哈希是否变化"
|
|
79
|
+
set +e
|
|
80
|
+
python "$CHECK_HASH_SCRIPT" --config "package.json" --check
|
|
81
|
+
rc_node=$?
|
|
82
|
+
set -e
|
|
83
|
+
if [ $rc_node -eq 1 ]; then
|
|
84
|
+
write_log "INFO" "依赖哈希已变化,执行:pnpm install"
|
|
85
|
+
pnpm install --registry=https://registry.npmmirror.com || write_log "ERROR" "Expo 项目依赖安装失败(pnpm 执行出错)"
|
|
86
|
+
|
|
87
|
+
write_log "INFO" "依赖哈希已变化,执行:npm run install-missing"
|
|
88
|
+
npm run install-missing || write_log "ERROR" "npm run install-missing 执行失败,请检查 package.json 中的脚本配置"
|
|
89
|
+
|
|
90
|
+
set +e
|
|
91
|
+
python "$CHECK_HASH_SCRIPT" --config "package.json" --update
|
|
92
|
+
set -e
|
|
93
|
+
else
|
|
94
|
+
write_log "INFO" "跳过 pnpm install 与 npm run install-missing"
|
|
95
|
+
fi
|
|
96
|
+
else
|
|
97
|
+
write_log "WARN" "未找到 check_hash.py,默认执行:pnpm install 与 npm run install-missing"
|
|
98
|
+
pnpm install --registry=https://registry.npmmirror.com || write_log "ERROR" "Expo 项目依赖安装失败(pnpm 执行出错)"
|
|
99
|
+
npm run install-missing || write_log "ERROR" "npm run install-missing 执行失败,请检查 package.json 中的脚本配置"
|
|
100
|
+
fi
|
|
101
|
+
|
|
102
|
+
write_log "INFO" "检查根目录 post_install.py"
|
|
103
|
+
if [ -f "$ROOT_DIR/post_install.py" ]; then
|
|
104
|
+
write_log "INFO" "执行:python $ROOT_DIR/post_install.py"
|
|
105
|
+
python "$ROOT_DIR/post_install.py" || write_log "ERROR" "post_install.py 执行失败"
|
|
106
|
+
fi
|
|
107
|
+
|
|
108
|
+
write_log "INFO" "==================== 依赖安装完成!====================\n"
|
|
109
|
+
write_log "INFO" "下一步:执行 ./deploy_run.sh 启动服务"
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
ROOT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
2
|
+
LOG_DIR="${COZE_LOG_DIR:-$ROOT_DIR/logs}"
|
|
3
|
+
LOG_FILE="$LOG_DIR/app.log"
|
|
4
|
+
mkdir -p "$LOG_DIR"
|
|
5
|
+
|
|
6
|
+
# ==================== 配置项 ====================
|
|
7
|
+
# Python 服务配置
|
|
8
|
+
SERVER_HOST="0.0.0.0"
|
|
9
|
+
SERVER_PORT="9091"
|
|
10
|
+
# Expo 项目配置
|
|
11
|
+
EXPO_HOST="0.0.0.0"
|
|
12
|
+
EXPO_DIR="expo"
|
|
13
|
+
EXPO_PORT="9090"
|
|
14
|
+
WEB_URL="${COZE_PROJECT_DOMAIN_DEFAULT:-http://127.0.0.1:${SERVER_PORT}}"
|
|
15
|
+
ASSUME_YES="0"
|
|
16
|
+
EXPO_PUBLIC_BACKEND_BASE_URL="${EXPO_PUBLIC_BACKEND_BASE_URL:-$WEB_URL}"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
EXPO_PACKAGER_PROXY_URL="${EXPO_PUBLIC_BACKEND_BASE_URL}"
|
|
20
|
+
export EXPO_PUBLIC_BACKEND_BASE_URL EXPO_PACKAGER_PROXY_URL
|
|
21
|
+
# 运行时变量(为避免 set -u 的未绑定错误,预置为空)
|
|
22
|
+
SERVER_PID=""
|
|
23
|
+
EXPO_PID=""
|
|
24
|
+
# ==================== 工具函数 ====================
|
|
25
|
+
info() {
|
|
26
|
+
echo -e "\033[32m[INFO] $1\033[0m"
|
|
27
|
+
}
|
|
28
|
+
warn() {
|
|
29
|
+
echo -e "\033[33m[WARN] $1\033[0m"
|
|
30
|
+
}
|
|
31
|
+
error() {
|
|
32
|
+
echo -e "\033[31m[ERROR] $1\033[0m"
|
|
33
|
+
exit 1
|
|
34
|
+
}
|
|
35
|
+
check_command() {
|
|
36
|
+
if ! command -v "$1" &> /dev/null; then
|
|
37
|
+
error "命令 $1 未找到,请先安装"
|
|
38
|
+
fi
|
|
39
|
+
}
|
|
40
|
+
while [ $# -gt 0 ]; do
|
|
41
|
+
case "$1" in
|
|
42
|
+
-y|--yes)
|
|
43
|
+
ASSUME_YES="1"
|
|
44
|
+
shift
|
|
45
|
+
;;
|
|
46
|
+
*)
|
|
47
|
+
shift
|
|
48
|
+
;;
|
|
49
|
+
esac
|
|
50
|
+
done
|
|
51
|
+
is_port_free() {
|
|
52
|
+
! lsof -iTCP:"$1" -sTCP:LISTEN >/dev/null 2>&1
|
|
53
|
+
}
|
|
54
|
+
choose_next_free_port() {
|
|
55
|
+
local start=$1
|
|
56
|
+
local p=$start
|
|
57
|
+
while ! is_port_free "$p"; do
|
|
58
|
+
p=$((p+1))
|
|
59
|
+
done
|
|
60
|
+
echo "$p"
|
|
61
|
+
}
|
|
62
|
+
ensure_port() {
|
|
63
|
+
local var_name=$1
|
|
64
|
+
local port_val=$2
|
|
65
|
+
if is_port_free "$port_val"; then
|
|
66
|
+
info "端口未占用:$port_val"
|
|
67
|
+
eval "$var_name=$port_val"
|
|
68
|
+
else
|
|
69
|
+
warn "端口已占用:$port_val"
|
|
70
|
+
local choice
|
|
71
|
+
if [ "$ASSUME_YES" = "1" ]; then choice="Y"; else read -r -p "是否关闭该端口的进程?[Y/n] " choice || choice="Y"; fi
|
|
72
|
+
if [ -z "$choice" ] || [ "$choice" = "y" ] || [ "$choice" = "Y" ]; then
|
|
73
|
+
if command -v lsof &> /dev/null; then
|
|
74
|
+
local pids
|
|
75
|
+
pids=$(lsof -t -i tcp:"$port_val" -sTCP:LISTEN 2>/dev/null || true)
|
|
76
|
+
if [ -n "$pids" ]; then
|
|
77
|
+
info "正在关闭进程:$pids"
|
|
78
|
+
kill -9 $pids 2>/dev/null || warn "关闭进程失败:$pids"
|
|
79
|
+
eval "$var_name=$port_val"
|
|
80
|
+
else
|
|
81
|
+
warn "未获取到占用该端口的进程"
|
|
82
|
+
eval "$var_name=$port_val"
|
|
83
|
+
fi
|
|
84
|
+
else
|
|
85
|
+
warn "缺少 lsof,无法自动关闭进程"
|
|
86
|
+
eval "$var_name=$port_val"
|
|
87
|
+
fi
|
|
88
|
+
else
|
|
89
|
+
local new_port
|
|
90
|
+
new_port=$(choose_next_free_port "$port_val")
|
|
91
|
+
info "使用新的端口:$new_port"
|
|
92
|
+
eval "$var_name=$new_port"
|
|
93
|
+
fi
|
|
94
|
+
fi
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
write_log() {
|
|
98
|
+
local level="${1:-INFO}"
|
|
99
|
+
local msg="${2:-}"
|
|
100
|
+
python - "$LOG_FILE" "$level" "$msg" <<'PY'
|
|
101
|
+
import os,sys,json,time
|
|
102
|
+
from datetime import datetime, timezone, timedelta
|
|
103
|
+
path, level, msg = sys.argv[1], sys.argv[2].upper(), sys.argv[3]
|
|
104
|
+
ts = int(time.time()*1000)
|
|
105
|
+
msg = msg.replace('\\n', '\n')
|
|
106
|
+
tz = timezone(timedelta(hours=8))
|
|
107
|
+
dt_str = datetime.now(tz).strftime('%Y-%m-%d %H:%M:%S')
|
|
108
|
+
msg = f"{dt_str} [{level}] {msg}"
|
|
109
|
+
with open(path,'a',encoding='utf-8',buffering=1) as f:
|
|
110
|
+
f.write(json.dumps({"level":level,"message":msg,"timestamp":ts},ensure_ascii=False)+'\n')
|
|
111
|
+
f.flush(); os.fsync(f.fileno())
|
|
112
|
+
PY
|
|
113
|
+
case "$level" in
|
|
114
|
+
INFO) echo -e "\033[32m[INFO] $msg\033[0m" ;;
|
|
115
|
+
WARN) echo -e "\033[33m[WARN] $msg\033[0m" ;;
|
|
116
|
+
ERROR) echo -e "\033[31m[ERROR] $msg\033[0m" ;;
|
|
117
|
+
*) echo -e "\033[32m[INFO] $msg\033[0m" ;;
|
|
118
|
+
esac
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
wait_port_connectable() {
|
|
122
|
+
local host=$1 port=$2 retries=${3:-10}
|
|
123
|
+
for _ in $(seq 1 "$retries"); do
|
|
124
|
+
nc -z "$host" "$port" && return 0
|
|
125
|
+
sleep 1
|
|
126
|
+
done
|
|
127
|
+
return 1
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
start_expo() {
|
|
131
|
+
local offline="${1:-0}"
|
|
132
|
+
if [ "$offline" = "1" ]; then
|
|
133
|
+
EXPO_OFFLINE=1 EXPO_NO_DOCTOR=1 EXPO_PUBLIC_BACKEND_BASE_URL="$EXPO_PUBLIC_BACKEND_BASE_URL" EXPO_PACKAGER_PROXY_URL="$EXPO_PACKAGER_PROXY_URL" nohup npx expo start --port "$EXPO_PORT" > logs/expo.log 2>&1 &
|
|
134
|
+
else
|
|
135
|
+
EXPO_NO_DOCTOR=1 EXPO_PUBLIC_BACKEND_BASE_URL="$EXPO_PUBLIC_BACKEND_BASE_URL" EXPO_PACKAGER_PROXY_URL="$EXPO_PACKAGER_PROXY_URL" nohup npx expo start --port "$EXPO_PORT" > logs/expo.log 2>&1 &
|
|
136
|
+
fi
|
|
137
|
+
EXPO_PID=$!
|
|
138
|
+
echo "$EXPO_PID"
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
detect_expo_fetch_failed() {
|
|
142
|
+
local timeout="${1:-8}"
|
|
143
|
+
local waited=0
|
|
144
|
+
local log_file="logs/expo.log"
|
|
145
|
+
while [ "$waited" -lt "$timeout" ]; do
|
|
146
|
+
if [ -f "$log_file" ] && grep -q "TypeError: fetch failed" "$log_file" 2>/dev/null; then
|
|
147
|
+
return 0
|
|
148
|
+
fi
|
|
149
|
+
sleep 1
|
|
150
|
+
waited=$((waited+1))
|
|
151
|
+
done
|
|
152
|
+
return 1
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
# ==================== 前置检查 ====================
|
|
156
|
+
write_log "INFO" "==================== 开始启动 ===================="
|
|
157
|
+
write_log "INFO" "开始执行服务启动脚本(start_dev.sh)..."
|
|
158
|
+
write_log "INFO" "正在检查依赖命令和目录是否存在..."
|
|
159
|
+
# 检查核心命令
|
|
160
|
+
check_command "python"
|
|
161
|
+
check_command "npm"
|
|
162
|
+
check_command "pnpm"
|
|
163
|
+
check_command "lsof"
|
|
164
|
+
check_command "bash"
|
|
165
|
+
|
|
166
|
+
info "准备日志目录:logs"
|
|
167
|
+
mkdir -p logs
|
|
168
|
+
# 端口占用预检查与处理
|
|
169
|
+
ensure_port SERVER_PORT "$SERVER_PORT"
|
|
170
|
+
ensure_port EXPO_PORT "$EXPO_PORT"
|
|
171
|
+
|
|
172
|
+
# ==================== 步骤 1:启动 Python 服务 ====================
|
|
173
|
+
|
|
174
|
+
# ------------环境变量----------------------
|
|
175
|
+
export storage_type="s3"
|
|
176
|
+
export project_platform="app"
|
|
177
|
+
|
|
178
|
+
if [ -n "${COZE_WORKLOAD_IDENTITY_API_KEY:-}" ]; then
|
|
179
|
+
export OPENAI_API_KEY="$COZE_WORKLOAD_IDENTITY_API_KEY"
|
|
180
|
+
fi
|
|
181
|
+
if [ -n "${OPENAI_API_KEY:-}" ]; then
|
|
182
|
+
export ARK_API_KEY="$OPENAI_API_KEY"
|
|
183
|
+
fi
|
|
184
|
+
|
|
185
|
+
if [ -n "${COZE_INTEGRATION_BASE_URL:-}" ]; then
|
|
186
|
+
_base="${COZE_INTEGRATION_BASE_URL%/}"
|
|
187
|
+
export default_llm_base_url="${_base}/api/v3"
|
|
188
|
+
else
|
|
189
|
+
export default_llm_base_url="https://ark.cn-beijing.volces.com/api/v3"
|
|
190
|
+
fi
|
|
191
|
+
|
|
192
|
+
if [ -n "${PGDATABASE_URL:-}" ]; then
|
|
193
|
+
_pg="${PGDATABASE_URL}"
|
|
194
|
+
case "${_pg}" in
|
|
195
|
+
postgresql://*)
|
|
196
|
+
_pg="postgresql+asyncpg://${_pg#postgresql://}"
|
|
197
|
+
;;
|
|
198
|
+
esac
|
|
199
|
+
_pg="${_pg%%\?*}"
|
|
200
|
+
export DATABASE_URL="${_pg}"
|
|
201
|
+
fi
|
|
202
|
+
|
|
203
|
+
# ------------环境变量----------------------
|
|
204
|
+
|
|
205
|
+
write_log "INFO" "==================== 启动 server 服务 ===================="
|
|
206
|
+
write_log "INFO" "正在执行:npm run server"
|
|
207
|
+
nohup npm run server > logs/app.log 2>&1 &
|
|
208
|
+
SERVER_PID=$!
|
|
209
|
+
if [ -z "${SERVER_PID}" ]; then
|
|
210
|
+
write_log "ERROR" "无法获取 server 后台进程 PID"
|
|
211
|
+
fi
|
|
212
|
+
write_log "INFO" "server 服务已启动,进程 ID:${SERVER_PID:-unknown}"
|
|
213
|
+
|
|
214
|
+
write_log "INFO" "==================== 启动 Expo 项目 ===================="
|
|
215
|
+
write_log "INFO" "正在执行:npx expo start --port ${EXPO_PORT}"
|
|
216
|
+
write_log "INFO" "开始启动 Expo 服务"
|
|
217
|
+
EXPO_PID=$(start_expo 0)
|
|
218
|
+
if detect_expo_fetch_failed 8; then
|
|
219
|
+
write_log "WARN" "Expo 启动检测到网络错误:TypeError: fetch failed,启用离线模式重试"
|
|
220
|
+
if [ -n "${EXPO_PID}" ]; then kill -9 "$EXPO_PID" 2>/dev/null || true; fi
|
|
221
|
+
: > logs/expo.log
|
|
222
|
+
EXPO_PID=$(start_expo 1)
|
|
223
|
+
fi
|
|
224
|
+
# 输出以下环境变量,确保 Expo 项目能正确连接到 Python 服务
|
|
225
|
+
write_log "INFO" "Expo 环境变量配置:"
|
|
226
|
+
write_log "INFO" "EXPO_PUBLIC_BACKEND_BASE_URL=${EXPO_PUBLIC_BACKEND_BASE_URL}"
|
|
227
|
+
write_log "INFO" "EXPO_PACKAGER_PROXY_URL=${EXPO_PACKAGER_PROXY_URL}"
|
|
228
|
+
if [ -z "${EXPO_PID}" ]; then
|
|
229
|
+
write_log "ERROR" "无法获取 Expo 后台进程 PID"
|
|
230
|
+
fi
|
|
231
|
+
|
|
232
|
+
write_log "INFO" "所有服务已启动。Server PID: ${SERVER_PID}, Expo PID: ${EXPO_PID}"
|
|
233
|
+
|
|
234
|
+
write_log "INFO" "检查 Server 服务端口:$SERVER_HOST:$SERVER_PORT"
|
|
235
|
+
if wait_port_connectable "$SERVER_HOST" "$SERVER_PORT" 10 2; then
|
|
236
|
+
write_log "INFO" "端口可连接:$SERVER_HOST:$SERVER_PORT"
|
|
237
|
+
else
|
|
238
|
+
write_log "WARN" "端口不可连接:$SERVER_HOST:$SERVER_PORT 10 次)"
|
|
239
|
+
fi
|
|
240
|
+
|
|
241
|
+
write_log "INFO" "检查 Expo 服务端口:$EXPO_HOST:$EXPO_PORT"
|
|
242
|
+
if wait_port_connectable "$EXPO_HOST" "$EXPO_PORT" 10 2; then
|
|
243
|
+
write_log "INFO" "端口可连接:$EXPO_HOST:$EXPO_PORT"
|
|
244
|
+
else
|
|
245
|
+
write_log "WARN" "端口不可连接:$EXPO_HOST:$EXPO_PORT(已尝试 10 次)"
|
|
246
|
+
fi
|
|
247
|
+
|
|
248
|
+
write_log "INFO" "服务端口检查完成"
|
|
249
|
+
|
|
250
|
+
write_log "INFO" "检查根目录 post_run.py"
|
|
251
|
+
if [ -f "$ROOT_DIR/post_run.py" ]; then
|
|
252
|
+
write_log "INFO" "启动检查中"
|
|
253
|
+
python "$ROOT_DIR/post_run.py" --port "$EXPO_PORT" || write_log "ERROR" "post_run.py 执行失败"
|
|
254
|
+
write_log "INFO" "启动检查结束"
|
|
255
|
+
fi
|
|
256
|
+
|
|
257
|
+
write_log "INFO" "==================== 服务启动完成 ===================="
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"expo": {
|
|
3
|
+
"name": "${app_name}",
|
|
4
|
+
"slug": "${slug}",
|
|
5
|
+
"version": "1.0.0",
|
|
6
|
+
"orientation": "portrait",
|
|
7
|
+
"icon": "./client/assets/images/icon.png",
|
|
8
|
+
"scheme": "myapp",
|
|
9
|
+
"userInterfaceStyle": "automatic",
|
|
10
|
+
"newArchEnabled": true,
|
|
11
|
+
"ios": {
|
|
12
|
+
"supportsTablet": true
|
|
13
|
+
},
|
|
14
|
+
"android": {
|
|
15
|
+
"adaptiveIcon": {
|
|
16
|
+
"foregroundImage": "./client/assets/images/adaptive-icon.png",
|
|
17
|
+
"backgroundColor": "#ffffff"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"web": {
|
|
21
|
+
"bundler": "metro",
|
|
22
|
+
"output": "static",
|
|
23
|
+
"favicon": "./client/assets/images/favicon.png"
|
|
24
|
+
},
|
|
25
|
+
"plugins": [
|
|
26
|
+
"expo-router",
|
|
27
|
+
[
|
|
28
|
+
"expo-splash-screen",
|
|
29
|
+
{
|
|
30
|
+
"image": "./client/assets/images/splash-icon.png",
|
|
31
|
+
"imageWidth": 200,
|
|
32
|
+
"resizeMode": "contain",
|
|
33
|
+
"backgroundColor": "#ffffff"
|
|
34
|
+
}
|
|
35
|
+
],
|
|
36
|
+
[
|
|
37
|
+
"expo-image-picker",
|
|
38
|
+
{
|
|
39
|
+
"photosPermission": "允许${app_name}访问您的相册,以便您上传或保存图片。",
|
|
40
|
+
"cameraPermission": "允许${app_name}使用您的相机,以便您直接拍摄照片上传。",
|
|
41
|
+
"microphonePermission": "允许${app_name}访问您的麦克风,以便您拍摄带有声音的视频。"
|
|
42
|
+
}
|
|
43
|
+
],
|
|
44
|
+
[
|
|
45
|
+
"expo-location",
|
|
46
|
+
{
|
|
47
|
+
"locationWhenInUsePermission": "${app_name}需要访问您的位置以提供周边服务及导航功能。"
|
|
48
|
+
}
|
|
49
|
+
],
|
|
50
|
+
[
|
|
51
|
+
"expo-camera",
|
|
52
|
+
{
|
|
53
|
+
"cameraPermission": "${app_name}需要访问相机以拍摄照片和视频。",
|
|
54
|
+
"microphonePermission": "${app_name}需要访问麦克风以录制视频声音。",
|
|
55
|
+
"recordAudioAndroid": true
|
|
56
|
+
}
|
|
57
|
+
]
|
|
58
|
+
],
|
|
59
|
+
"experiments": {
|
|
60
|
+
"typedRoutes": true
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Tabs } from 'expo-router';
|
|
2
|
+
import { FontAwesome6 } from '@expo/vector-icons';
|
|
3
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
4
|
+
import { useTheme } from '@/hooks/useTheme';
|
|
5
|
+
|
|
6
|
+
export default function TabLayout() {
|
|
7
|
+
const insets = useSafeAreaInsets();
|
|
8
|
+
const { theme } = useTheme();
|
|
9
|
+
const tabBarHeight = 56 + Math.max(insets.bottom, 8);
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<Tabs
|
|
13
|
+
backBehavior='history'
|
|
14
|
+
screenOptions={{
|
|
15
|
+
headerShown: false,
|
|
16
|
+
tabBarActiveTintColor: theme.tabIconSelected,
|
|
17
|
+
tabBarInactiveTintColor: theme.tabIconDefault,
|
|
18
|
+
tabBarStyle: {
|
|
19
|
+
backgroundColor: theme.backgroundRoot,
|
|
20
|
+
borderTopColor: theme.backgroundSecondary,
|
|
21
|
+
borderTopWidth: 1,
|
|
22
|
+
height: tabBarHeight,
|
|
23
|
+
paddingBottom: Math.max(insets.bottom, 8),
|
|
24
|
+
paddingTop: 6,
|
|
25
|
+
},
|
|
26
|
+
}}
|
|
27
|
+
>
|
|
28
|
+
<Tabs.Screen
|
|
29
|
+
name='index'
|
|
30
|
+
options={{ href: null }}
|
|
31
|
+
/>
|
|
32
|
+
<Tabs.Screen
|
|
33
|
+
name='home'
|
|
34
|
+
options={{
|
|
35
|
+
title: '首页',
|
|
36
|
+
tabBarIcon: ({ color }) => (
|
|
37
|
+
<FontAwesome6 name='house' size={20} color={color} />
|
|
38
|
+
),
|
|
39
|
+
}}
|
|
40
|
+
/>
|
|
41
|
+
</Tabs>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from "@/screens/home";
|