Compare commits
4
Commits
main
..
6565ad81b5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6565ad81b5 | ||
|
|
f57fe229fc | ||
|
|
a99248b284 | ||
|
|
5088313f74 |
No files matched your search
@@ -0,0 +1,9 @@
|
||||
.dockerignore
|
||||
Dockerfile
|
||||
build/
|
||||
.dart_tool/
|
||||
.git/
|
||||
.github/
|
||||
.gitignore
|
||||
.idea/
|
||||
.packages
|
||||
+11
-22
@@ -1,32 +1,21 @@
|
||||
# Dart
|
||||
# https://dart.dev/guides/libraries/private-files
|
||||
# Created by `dart pub`
|
||||
.dart_tool/
|
||||
.packages
|
||||
|
||||
# ObjectBox generated
|
||||
*.g.dart
|
||||
objectbox-model.json
|
||||
|
||||
# Build
|
||||
# Downloads
|
||||
download/
|
||||
|
||||
# Node
|
||||
node_modules/
|
||||
|
||||
# Build outputs
|
||||
.next/
|
||||
dist/
|
||||
build/
|
||||
|
||||
# Environment
|
||||
.env
|
||||
.env.local
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
*.iml
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Conversation exports (local only)
|
||||
conversation_export/
|
||||
uploads/
|
||||
objectbox/
|
||||
objectbox.dll
|
||||
|
||||
# AI
|
||||
.claude
|
||||
Executable
+117
@@ -0,0 +1,117 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 将 stderr 重定向到 stdout,避免 execute_command 因为 stderr 输出而报错
|
||||
exec 2>&1
|
||||
|
||||
set -e
|
||||
|
||||
# 获取脚本所在目录(.zscripts 目录,即 workspace-agent/.zscripts)
|
||||
# 使用 $0 获取脚本路径(兼容 sh 和 bash)
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
|
||||
# Next.js 项目路径
|
||||
NEXTJS_PROJECT_DIR="/home/z/my-project"
|
||||
|
||||
# 检查 Next.js 项目目录是否存在
|
||||
if [ ! -d "$NEXTJS_PROJECT_DIR" ]; then
|
||||
echo "❌ 错误: Next.js 项目目录不存在: $NEXTJS_PROJECT_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "🚀 开始构建 Next.js 应用和 mini-services..."
|
||||
echo "📁 Next.js 项目路径: $NEXTJS_PROJECT_DIR"
|
||||
|
||||
# 切换到 Next.js 项目目录
|
||||
cd "$NEXTJS_PROJECT_DIR" || exit 1
|
||||
|
||||
# 设置环境变量
|
||||
export NEXT_TELEMETRY_DISABLED=1
|
||||
|
||||
BUILD_DIR="/tmp/build_fullstack_$BUILD_ID"
|
||||
echo "📁 清理并创建构建目录: $BUILD_DIR"
|
||||
mkdir -p "$BUILD_DIR"
|
||||
|
||||
# 安装依赖
|
||||
echo "📦 安装依赖..."
|
||||
bun install
|
||||
|
||||
# 构建 Next.js 应用
|
||||
echo "🔨 构建 Next.js 应用..."
|
||||
bun run build
|
||||
|
||||
# 构建 mini-services
|
||||
# 检查 Next.js 项目目录下是否有 mini-services 目录
|
||||
if [ -d "$NEXTJS_PROJECT_DIR/mini-services" ]; then
|
||||
echo "🔨 构建 mini-services..."
|
||||
# 使用 workspace-agent 目录下的 mini-services 脚本
|
||||
sh "$SCRIPT_DIR/mini-services-install.sh"
|
||||
sh "$SCRIPT_DIR/mini-services-build.sh"
|
||||
|
||||
# 复制 mini-services-start.sh 到 mini-services-dist 目录
|
||||
echo " - 复制 mini-services-start.sh 到 $BUILD_DIR"
|
||||
cp "$SCRIPT_DIR/mini-services-start.sh" "$BUILD_DIR/mini-services-start.sh"
|
||||
chmod +x "$BUILD_DIR/mini-services-start.sh"
|
||||
else
|
||||
echo "ℹ️ mini-services 目录不存在,跳过"
|
||||
fi
|
||||
|
||||
# 将所有构建产物复制到临时构建目录
|
||||
echo "📦 收集构建产物到 $BUILD_DIR..."
|
||||
|
||||
# 复制 Next.js standalone 构建输出
|
||||
if [ -d ".next/standalone" ]; then
|
||||
echo " - 复制 .next/standalone"
|
||||
cp -r .next/standalone "$BUILD_DIR/next-service-dist/"
|
||||
fi
|
||||
|
||||
# 复制 Next.js 静态文件
|
||||
if [ -d ".next/static" ]; then
|
||||
echo " - 复制 .next/static"
|
||||
mkdir -p "$BUILD_DIR/next-service-dist/.next"
|
||||
cp -r .next/static "$BUILD_DIR/next-service-dist/.next/"
|
||||
fi
|
||||
|
||||
# 复制 public 目录
|
||||
if [ -d "public" ]; then
|
||||
echo " - 复制 public"
|
||||
cp -r public "$BUILD_DIR/next-service-dist/"
|
||||
fi
|
||||
|
||||
# 最后再迁移数据库到 BUILD_DIR/db
|
||||
if [ "$(ls -A ./db 2>/dev/null)" ]; then
|
||||
echo "🗄️ 检测到数据库文件,运行数据库迁移..."
|
||||
DATABASE_URL=file:$BUILD_DIR/db/custom.db bun run db:push
|
||||
echo "✅ 数据库迁移完成"
|
||||
ls -lah $BUILD_DIR/db
|
||||
else
|
||||
echo "ℹ️ db 目录为空,跳过数据库迁移"
|
||||
fi
|
||||
|
||||
# 复制 Caddyfile(如果存在)
|
||||
if [ -f "Caddyfile" ]; then
|
||||
echo " - 复制 Caddyfile"
|
||||
cp Caddyfile "$BUILD_DIR/"
|
||||
else
|
||||
echo "ℹ️ Caddyfile 不存在,跳过"
|
||||
fi
|
||||
|
||||
# 复制 start.sh 脚本
|
||||
echo " - 复制 start.sh 到 $BUILD_DIR"
|
||||
cp "$SCRIPT_DIR/start.sh" "$BUILD_DIR/start.sh"
|
||||
chmod +x "$BUILD_DIR/start.sh"
|
||||
|
||||
# 打包到 $BUILD_DIR.tar.gz
|
||||
PACKAGE_FILE="${BUILD_DIR}.tar.gz"
|
||||
echo ""
|
||||
echo "📦 打包构建产物到 $PACKAGE_FILE..."
|
||||
cd "$BUILD_DIR" || exit 1
|
||||
tar -czf "$PACKAGE_FILE" .
|
||||
cd - > /dev/null || exit 1
|
||||
|
||||
# # 清理临时目录
|
||||
# rm -rf "$BUILD_DIR"
|
||||
|
||||
echo ""
|
||||
echo "✅ 构建完成!所有产物已打包到 $PACKAGE_FILE"
|
||||
echo "📊 打包文件大小:"
|
||||
ls -lh "$PACKAGE_FILE"
|
||||
Executable
+78
@@ -0,0 +1,78 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 配置项
|
||||
ROOT_DIR="/home/z/my-project/mini-services"
|
||||
DIST_DIR="/tmp/build_fullstack_$BUILD_ID/mini-services-dist"
|
||||
|
||||
main() {
|
||||
echo "🚀 开始批量构建..."
|
||||
|
||||
# 检查 rootdir 是否存在
|
||||
if [ ! -d "$ROOT_DIR" ]; then
|
||||
echo "ℹ️ 目录 $ROOT_DIR 不存在,跳过构建"
|
||||
return
|
||||
fi
|
||||
|
||||
# 创建输出目录(如果不存在)
|
||||
mkdir -p "$DIST_DIR"
|
||||
|
||||
# 统计变量
|
||||
success_count=0
|
||||
fail_count=0
|
||||
|
||||
# 遍历 mini-services 目录下的所有文件夹
|
||||
for dir in "$ROOT_DIR"/*; do
|
||||
# 检查是否是目录且包含 package.json
|
||||
if [ -d "$dir" ] && [ -f "$dir/package.json" ]; then
|
||||
project_name=$(basename "$dir")
|
||||
|
||||
# 智能查找入口文件 (按优先级查找)
|
||||
entry_path=""
|
||||
for entry in "src/index.ts" "index.ts" "src/index.js" "index.js"; do
|
||||
if [ -f "$dir/$entry" ]; then
|
||||
entry_path="$dir/$entry"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "$entry_path" ]; then
|
||||
echo "⚠️ 跳过 $project_name: 未找到入口文件 (index.ts/js)"
|
||||
continue
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "📦 正在构建: $project_name..."
|
||||
|
||||
# 使用 bun build CLI 构建
|
||||
output_file="$DIST_DIR/mini-service-$project_name.js"
|
||||
|
||||
if bun build "$entry_path" \
|
||||
--outfile "$output_file" \
|
||||
--target bun \
|
||||
--minify; then
|
||||
echo "✅ $project_name 构建成功 -> $output_file"
|
||||
success_count=$((success_count + 1))
|
||||
else
|
||||
echo "❌ $project_name 构建失败"
|
||||
fail_count=$((fail_count + 1))
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -f ./.zscripts/mini-services-start.sh ]; then
|
||||
cp ./.zscripts/mini-services-start.sh "$DIST_DIR/mini-services-start.sh"
|
||||
chmod +x "$DIST_DIR/mini-services-start.sh"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "🎉 所有任务完成!"
|
||||
if [ $success_count -gt 0 ] || [ $fail_count -gt 0 ]; then
|
||||
echo "✅ 成功: $success_count 个"
|
||||
if [ $fail_count -gt 0 ]; then
|
||||
echo "❌ 失败: $fail_count 个"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
main
|
||||
|
||||
Executable
+65
@@ -0,0 +1,65 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 配置项
|
||||
ROOT_DIR="/home/z/my-project/mini-services"
|
||||
|
||||
main() {
|
||||
echo "🚀 开始批量安装依赖..."
|
||||
|
||||
# 检查 rootdir 是否存在
|
||||
if [ ! -d "$ROOT_DIR" ]; then
|
||||
echo "ℹ️ 目录 $ROOT_DIR 不存在,跳过安装"
|
||||
return
|
||||
fi
|
||||
|
||||
# 统计变量
|
||||
success_count=0
|
||||
fail_count=0
|
||||
failed_projects=""
|
||||
|
||||
# 遍历 mini-services 目录下的所有文件夹
|
||||
for dir in "$ROOT_DIR"/*; do
|
||||
# 检查是否是目录且包含 package.json
|
||||
if [ -d "$dir" ] && [ -f "$dir/package.json" ]; then
|
||||
project_name=$(basename "$dir")
|
||||
echo ""
|
||||
echo "📦 正在安装依赖: $project_name..."
|
||||
|
||||
# 进入项目目录并执行 bun install
|
||||
if (cd "$dir" && bun install); then
|
||||
echo "✅ $project_name 依赖安装成功"
|
||||
success_count=$((success_count + 1))
|
||||
else
|
||||
echo "❌ $project_name 依赖安装失败"
|
||||
fail_count=$((fail_count + 1))
|
||||
if [ -z "$failed_projects" ]; then
|
||||
failed_projects="$project_name"
|
||||
else
|
||||
failed_projects="$failed_projects $project_name"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
# 汇总结果
|
||||
echo ""
|
||||
echo "=================================================="
|
||||
if [ $success_count -gt 0 ] || [ $fail_count -gt 0 ]; then
|
||||
echo "🎉 安装完成!"
|
||||
echo "✅ 成功: $success_count 个"
|
||||
if [ $fail_count -gt 0 ]; then
|
||||
echo "❌ 失败: $fail_count 个"
|
||||
echo ""
|
||||
echo "失败的项目:"
|
||||
for project in $failed_projects; do
|
||||
echo " - $project"
|
||||
done
|
||||
fi
|
||||
else
|
||||
echo "ℹ️ 未找到任何包含 package.json 的项目"
|
||||
fi
|
||||
echo "=================================================="
|
||||
}
|
||||
|
||||
main
|
||||
|
||||
Executable
+123
@@ -0,0 +1,123 @@
|
||||
#!/bin/sh
|
||||
|
||||
# 配置项
|
||||
DIST_DIR="./mini-services-dist"
|
||||
|
||||
# 存储所有子进程的 PID
|
||||
pids=""
|
||||
|
||||
# 清理函数:优雅关闭所有服务
|
||||
cleanup() {
|
||||
echo ""
|
||||
echo "🛑 正在关闭所有服务..."
|
||||
|
||||
# 发送 SIGTERM 信号给所有子进程
|
||||
for pid in $pids; do
|
||||
if kill -0 "$pid" 2>/dev/null; then
|
||||
service_name=$(ps -p "$pid" -o comm= 2>/dev/null || echo "unknown")
|
||||
echo " 关闭进程 $pid ($service_name)..."
|
||||
kill -TERM "$pid" 2>/dev/null
|
||||
fi
|
||||
done
|
||||
|
||||
# 等待所有进程退出(最多等待 5 秒)
|
||||
sleep 1
|
||||
for pid in $pids; do
|
||||
if kill -0 "$pid" 2>/dev/null; then
|
||||
# 如果还在运行,等待最多 4 秒
|
||||
timeout=4
|
||||
while [ $timeout -gt 0 ] && kill -0 "$pid" 2>/dev/null; do
|
||||
sleep 1
|
||||
timeout=$((timeout - 1))
|
||||
done
|
||||
# 如果仍然在运行,强制关闭
|
||||
if kill -0 "$pid" 2>/dev/null; then
|
||||
echo " 强制关闭进程 $pid..."
|
||||
kill -KILL "$pid" 2>/dev/null
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
echo "✅ 所有服务已关闭"
|
||||
}
|
||||
|
||||
main() {
|
||||
echo "🚀 开始启动所有 mini services..."
|
||||
|
||||
# 检查 dist 目录是否存在
|
||||
if [ ! -d "$DIST_DIR" ]; then
|
||||
echo "ℹ️ 目录 $DIST_DIR 不存在"
|
||||
return
|
||||
fi
|
||||
|
||||
# 查找所有 mini-service-*.js 文件
|
||||
service_files=""
|
||||
for file in "$DIST_DIR"/mini-service-*.js; do
|
||||
if [ -f "$file" ]; then
|
||||
if [ -z "$service_files" ]; then
|
||||
service_files="$file"
|
||||
else
|
||||
service_files="$service_files $file"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
# 计算服务文件数量
|
||||
service_count=0
|
||||
for file in $service_files; do
|
||||
service_count=$((service_count + 1))
|
||||
done
|
||||
|
||||
if [ $service_count -eq 0 ]; then
|
||||
echo "ℹ️ 未找到任何 mini service 文件"
|
||||
return
|
||||
fi
|
||||
|
||||
echo "📦 找到 $service_count 个服务,开始启动..."
|
||||
echo ""
|
||||
|
||||
# 启动每个服务
|
||||
for file in $service_files; do
|
||||
service_name=$(basename "$file" .js | sed 's/mini-service-//')
|
||||
echo "▶️ 启动服务: $service_name..."
|
||||
|
||||
# 使用 bun 运行服务(后台运行)
|
||||
bun "$file" &
|
||||
pid=$!
|
||||
if [ -z "$pids" ]; then
|
||||
pids="$pid"
|
||||
else
|
||||
pids="$pids $pid"
|
||||
fi
|
||||
|
||||
# 等待一小段时间检查进程是否成功启动
|
||||
sleep 0.5
|
||||
if ! kill -0 "$pid" 2>/dev/null; then
|
||||
echo "❌ $service_name 启动失败"
|
||||
# 从字符串中移除失败的 PID
|
||||
pids=$(echo "$pids" | sed "s/\b$pid\b//" | sed 's/ */ /g' | sed 's/^ *//' | sed 's/ *$//')
|
||||
else
|
||||
echo "✅ $service_name 已启动 (PID: $pid)"
|
||||
fi
|
||||
done
|
||||
|
||||
# 计算运行中的服务数量
|
||||
running_count=0
|
||||
for pid in $pids; do
|
||||
if kill -0 "$pid" 2>/dev/null; then
|
||||
running_count=$((running_count + 1))
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "🎉 所有服务已启动!共 $running_count 个服务正在运行"
|
||||
echo ""
|
||||
echo "💡 按 Ctrl+C 停止所有服务"
|
||||
echo ""
|
||||
|
||||
# 等待所有后台进程
|
||||
wait
|
||||
}
|
||||
|
||||
main
|
||||
|
||||
Executable
+126
@@ -0,0 +1,126 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -e
|
||||
|
||||
# 获取脚本所在目录
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
BUILD_DIR="$SCRIPT_DIR"
|
||||
|
||||
# 存储所有子进程的 PID
|
||||
pids=""
|
||||
|
||||
# 清理函数:优雅关闭所有服务
|
||||
cleanup() {
|
||||
echo ""
|
||||
echo "🛑 正在关闭所有服务..."
|
||||
|
||||
# 发送 SIGTERM 信号给所有子进程
|
||||
for pid in $pids; do
|
||||
if kill -0 "$pid" 2>/dev/null; then
|
||||
service_name=$(ps -p "$pid" -o comm= 2>/dev/null || echo "unknown")
|
||||
echo " 关闭进程 $pid ($service_name)..."
|
||||
kill -TERM "$pid" 2>/dev/null
|
||||
fi
|
||||
done
|
||||
|
||||
# 等待所有进程退出(最多等待 5 秒)
|
||||
sleep 1
|
||||
for pid in $pids; do
|
||||
if kill -0 "$pid" 2>/dev/null; then
|
||||
# 如果还在运行,等待最多 4 秒
|
||||
timeout=4
|
||||
while [ $timeout -gt 0 ] && kill -0 "$pid" 2>/dev/null; do
|
||||
sleep 1
|
||||
timeout=$((timeout - 1))
|
||||
done
|
||||
# 如果仍然在运行,强制关闭
|
||||
if kill -0 "$pid" 2>/dev/null; then
|
||||
echo " 强制关闭进程 $pid..."
|
||||
kill -KILL "$pid" 2>/dev/null
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
echo "✅ 所有服务已关闭"
|
||||
exit 0
|
||||
}
|
||||
|
||||
echo "🚀 开始启动所有服务..."
|
||||
echo ""
|
||||
|
||||
# 切换到构建目录
|
||||
cd "$BUILD_DIR" || exit 1
|
||||
|
||||
ls -lah
|
||||
|
||||
# 初始化数据库(如果存在)
|
||||
if [ -d "./next-service-dist/db" ] && [ "$(ls -A ./next-service-dist/db 2>/dev/null)" ] && [ -d "/db" ]; then
|
||||
echo "🗄️ 初始化数据库从 ./next-service-dist/db 到 /db..."
|
||||
cp -r ./next-service-dist/db/* /db/ 2>/dev/null || echo " ⚠️ 无法复制到 /db,跳过数据库初始化"
|
||||
echo "✅ 数据库初始化完成"
|
||||
fi
|
||||
|
||||
# 启动 Next.js 服务器
|
||||
if [ -f "./next-service-dist/server.js" ]; then
|
||||
echo "🚀 启动 Next.js 服务器..."
|
||||
cd next-service-dist/ || exit 1
|
||||
|
||||
# 设置环境变量
|
||||
export NODE_ENV=production
|
||||
export PORT=${PORT:-3000}
|
||||
export HOSTNAME=${HOSTNAME:-0.0.0.0}
|
||||
|
||||
# 后台启动 Next.js
|
||||
bun server.js &
|
||||
NEXT_PID=$!
|
||||
pids="$NEXT_PID"
|
||||
|
||||
# 等待一小段时间检查进程是否成功启动
|
||||
sleep 1
|
||||
if ! kill -0 "$NEXT_PID" 2>/dev/null; then
|
||||
echo "❌ Next.js 服务器启动失败"
|
||||
exit 1
|
||||
else
|
||||
echo "✅ Next.js 服务器已启动 (PID: $NEXT_PID, Port: $PORT)"
|
||||
fi
|
||||
|
||||
cd ../
|
||||
else
|
||||
echo "⚠️ 未找到 Next.js 服务器文件: ./next-service-dist/server.js"
|
||||
fi
|
||||
|
||||
# 启动 mini-services
|
||||
if [ -f "./mini-services-start.sh" ]; then
|
||||
echo "🚀 启动 mini-services..."
|
||||
|
||||
# 运行启动脚本(从根目录运行,脚本内部会处理 mini-services-dist 目录)
|
||||
sh ./mini-services-start.sh &
|
||||
MINI_PID=$!
|
||||
pids="$pids $MINI_PID"
|
||||
|
||||
# 等待一小段时间检查进程是否成功启动
|
||||
sleep 1
|
||||
if ! kill -0 "$MINI_PID" 2>/dev/null; then
|
||||
echo "⚠️ mini-services 可能启动失败,但继续运行..."
|
||||
else
|
||||
echo "✅ mini-services 已启动 (PID: $MINI_PID)"
|
||||
fi
|
||||
elif [ -d "./mini-services-dist" ]; then
|
||||
echo "⚠️ 未找到 mini-services 启动脚本,但目录存在"
|
||||
else
|
||||
echo "ℹ️ mini-services 目录不存在,跳过"
|
||||
fi
|
||||
|
||||
# 启动 Caddy(如果存在 Caddyfile)
|
||||
echo "🚀 启动 Caddy..."
|
||||
|
||||
# Caddy 作为前台进程运行(主进程)
|
||||
echo "✅ Caddy 已启动(前台运行)"
|
||||
echo ""
|
||||
echo "🎉 所有服务已启动!"
|
||||
echo ""
|
||||
echo "💡 按 Ctrl+C 停止所有服务"
|
||||
echo ""
|
||||
|
||||
# Caddy 作为主进程运行
|
||||
exec caddy run --config Caddyfile --adapter caddyfile
|
||||
+224
-138
@@ -1,162 +1,248 @@
|
||||
# Reliquary Server Architecture
|
||||
# Chat Server Architecture - Complete Reference
|
||||
|
||||
## 1. Overview
|
||||
Reliquary is a self-hosted chat server with:
|
||||
- JWT auth
|
||||
- ObjectBox persistence
|
||||
- WebSocket gateway fanout
|
||||
- Component-based messages
|
||||
- Decoupled upload pipeline (upload first, message commit anytime)
|
||||
## Project Overview
|
||||
|
||||
## 2. Identity and Roles (Current)
|
||||
- `userId` is an immutable opaque identifier (host-independent).
|
||||
- `handle` is derived metadata: `@username@home`.
|
||||
- `ChatUser.roles` controls authorization (`admin`, `member`, etc.).
|
||||
- `GET /api/me` returns `roles` so the client can gate admin UI.
|
||||
Self-hosted Discord alternative with decentralized identity. Each user has a "home server" where their identity lives, and can join other "guest servers" seamlessly using cryptographic signatures.
|
||||
|
||||
### Runtime Config Split
|
||||
- `server.bind_host` + `server.bind_port`: where Relic listens locally.
|
||||
- `server.public_base_url`: canonical external URL used for identity/handles/cross-server metadata.
|
||||
- This avoids conflating local bind settings with public identity/routing.
|
||||
## Core Architecture Decisions
|
||||
|
||||
### First Admin Bootstrap
|
||||
- Config key: `server.first_setup` in `config.yaml`.
|
||||
- When `first_setup: true`, the first registered local user gets `admin`.
|
||||
- Later registrations remain `member`.
|
||||
### Home Server Model
|
||||
- **NO identity server** - just use one regular server as "home"
|
||||
- **NO federation protocol** - servers don't talk to each other
|
||||
- **Client does everything** - syncs, cross-server requests, profile updates
|
||||
|
||||
## 3. Storage Model
|
||||
### Identity System
|
||||
- Ed25519 keypair generated on registration
|
||||
- Private key = proof of identity
|
||||
- Public key stored on home server at `/.well-known/users/:username`
|
||||
- userId format: `username@serverhost`
|
||||
|
||||
### 3.1 Entities
|
||||
- `ChatUser`: identity/profile/auth data.
|
||||
- `ChatChannel`: channel metadata.
|
||||
- `ChatMessage`: channel message, search text, and `componentsJson`.
|
||||
- `ChatFile`: canonical media object for:
|
||||
- message attachments
|
||||
- user avatar media
|
||||
- server icon/banner media
|
||||
- `ServerSettings`: singleton row for mutable server presentation settings.
|
||||
### Single Sign-On Flow
|
||||
1. User registers/logs in ONCE on home server
|
||||
2. Gets JWT token + encrypted keypair
|
||||
3. To join other servers: sign a message with private key
|
||||
4. Other server verifies signature against home server's public key
|
||||
5. No password ever sent to guest servers
|
||||
|
||||
### 3.2 ChatFile Fields (Important)
|
||||
- `attachmentId` (unique external ID; API-facing)
|
||||
- `originalFileName` (preserved filename metadata)
|
||||
- `storedFileName` (physical disk key)
|
||||
- `sha256`
|
||||
- `mimeType`, `sizeBytes`, `width`, `height`
|
||||
- `usageType`: `message | avatar | server_icon | server_banner`
|
||||
- `status`: `pending | ready | failed`
|
||||
---
|
||||
|
||||
## 4. Upload and Message Flow
|
||||
## What's Implemented
|
||||
|
||||
### 4.1 Recommended Client Flow (Implemented Server Contract)
|
||||
1. Client stages attachment in composer/component editor.
|
||||
2. Client calls `POST /api/uploads/init` to create pending upload.
|
||||
3. Client uploads bytes to `PUT /api/uploads/:uploadId`.
|
||||
4. Client sends `POST /api/messages` with components referencing `uploadId`/`attachmentId`.
|
||||
5. If upload completes after message create, server emits `message.updated`.
|
||||
### ✅ Done
|
||||
- User model with keypair, home server reference
|
||||
- Channel and Message models
|
||||
- Crypto service (keypair generation, signing, verification)
|
||||
- JWT authentication middleware
|
||||
- Register handler (creates keypair)
|
||||
- Login handler (returns encrypted keypair for device sync)
|
||||
- Join server handler (signature-based verification - stub)
|
||||
- Public endpoints for cross-server identity verification
|
||||
- Channel CRUD
|
||||
- Message CRUD with author caching
|
||||
|
||||
### 4.2 Why This Flow
|
||||
- No chronological jumping for text.
|
||||
- Immediate local optimistic UI.
|
||||
- Progressive media readiness via gateway updates.
|
||||
- Server keeps authoritative metadata and storage consistency.
|
||||
### ❌ Not Implemented
|
||||
- HTTP client for cross-server public key fetching
|
||||
- WebSocket for real-time messages
|
||||
- Profile sync (push updates to all servers)
|
||||
- Sticker packs (future feature)
|
||||
- File uploads
|
||||
- Permissions system (roles exist but not enforced)
|
||||
- Message editing/deletion permissions
|
||||
|
||||
## 5. API Surface
|
||||
---
|
||||
|
||||
### 5.0 Identity Contract
|
||||
- Registration returns:
|
||||
- `userId` (opaque stable ID)
|
||||
- `username`
|
||||
- `handle` (computed, mutable as home host changes)
|
||||
- Cross-server join request now expects:
|
||||
- `userId`
|
||||
- `username`
|
||||
- `homeServer`
|
||||
- `timestamp`
|
||||
- `signature` over `userId:timestamp`
|
||||
## Database Models
|
||||
|
||||
### 5.1 Uploads and Attachments
|
||||
- `POST /api/uploads/init` (auth)
|
||||
- `PUT /api/uploads/:uploadId` (auth)
|
||||
- `POST /api/uploads` (auth, legacy single-step compatibility)
|
||||
- `GET /attachments/:attachmentId` (public immutable media URL)
|
||||
- `GET /api/channels/:channelId/media` (auth, gallery index feed)
|
||||
```dart
|
||||
// User - works for both HOME and GUEST servers
|
||||
@Entity()
|
||||
class User {
|
||||
int id;
|
||||
String userId; // "alice@homeserver.com"
|
||||
String username;
|
||||
String? displayName, avatarUrl, bio;
|
||||
|
||||
String publicKey; // Ed25519, always stored
|
||||
|
||||
// HOME SERVER ONLY:
|
||||
String? passwordHash;
|
||||
String? encryptedPrivateKey; // For device sync
|
||||
String? keyPairSalt;
|
||||
|
||||
bool isLocal; // true = this is home server
|
||||
String? homeServer; // if guest, where is home?
|
||||
List<String> roles; // Server-specific
|
||||
DateTime createdAt, lastSyncedAt;
|
||||
}
|
||||
|
||||
### 5.2 Profile and Server Media
|
||||
- `PATCH /api/me` (auth): update profile text fields.
|
||||
- `PUT /api/me/avatar` (auth): bind uploaded media as avatar.
|
||||
- `GET /avatars/:userId` (public alias for current avatar).
|
||||
- `GET /icons/:serverId` (public alias for current server icon).
|
||||
- `GET /banners/:serverId` (public alias for current server banner).
|
||||
@Entity()
|
||||
class Channel {
|
||||
int id;
|
||||
String name;
|
||||
String? description;
|
||||
DateTime createdAt;
|
||||
}
|
||||
|
||||
### 5.3 Server Settings
|
||||
- `GET /api/server/settings` (auth)
|
||||
- `PATCH /api/server/settings` (admin)
|
||||
- `PUT /api/server/icon` (admin)
|
||||
- `PUT /api/server/banner` (admin)
|
||||
|
||||
## 6. Gateway Behavior
|
||||
- Existing events:
|
||||
- `message.created`
|
||||
- `message.updated`
|
||||
- `message.deleted`
|
||||
- For upload-driven updates:
|
||||
- A message can be created with media items in `uploading`.
|
||||
- When upload becomes ready, server rewrites affected components and emits `message.updated`.
|
||||
|
||||
## 7. Component Media Item Contract (Client)
|
||||
Each media item in a component should carry stable IDs and upload state:
|
||||
|
||||
```json
|
||||
{
|
||||
"itemId": "local_or_stable_id",
|
||||
"source": "local_upload|external_url",
|
||||
"uploadId": "pending_or_ready_attachment_id",
|
||||
"attachmentId": "same_as_uploadId_when_known",
|
||||
"status": "uploading|ready|failed",
|
||||
"url": "/attachments/<attachmentId>",
|
||||
"originalFileName": "image.png",
|
||||
"mimeType": "image/webp",
|
||||
"sizeBytes": 12345,
|
||||
"width": 1280,
|
||||
"height": 720,
|
||||
"thumbHash": "optional"
|
||||
@Entity()
|
||||
class Message {
|
||||
int id;
|
||||
int channelId;
|
||||
|
||||
// Author info (includes cross-server fetch capability)
|
||||
int authorLocalId;
|
||||
String authorUserId; // "alice@home.server"
|
||||
String? authorHomeServer;
|
||||
|
||||
// Cached snapshot at message time
|
||||
String authorUsername;
|
||||
String? authorAvatarUrl;
|
||||
|
||||
String content;
|
||||
DateTime createdAt, editedAt;
|
||||
}
|
||||
```
|
||||
|
||||
### 7.1 Validation Rules (Server)
|
||||
- Every component node must include:
|
||||
- `id` (string)
|
||||
- `type` (string)
|
||||
- `typeVersion` (positive int)
|
||||
- `data` (object)
|
||||
- Nested components are supported under `data.children` with depth/size limits.
|
||||
- Media item rules:
|
||||
- `source=local_upload` => `attachmentId` required.
|
||||
- `source=external_url` => valid `http/https` `url` required.
|
||||
- Invalid component payloads are rejected server-side.
|
||||
---
|
||||
|
||||
## 8. Client Implementation Notes
|
||||
## API Endpoints
|
||||
|
||||
### 8.1 Staging and Transform Choice
|
||||
On file pick/drop, choose transform mode before upload starts:
|
||||
- `Compressed WebP` (recommended default)
|
||||
- `WebP`
|
||||
- `Original`
|
||||
### Public (No Auth)
|
||||
```
|
||||
POST /auth/register → Create account on HOME server (generates keypair)
|
||||
POST /auth/login → Login to HOME server (returns keypair backup)
|
||||
POST /auth/join → Join THIS server with signature (no password)
|
||||
|
||||
If user changes mode for a staged file:
|
||||
- cancel in-flight upload
|
||||
- reprocess
|
||||
- re-upload
|
||||
GET /.well-known/users/:username → Public profile + public key
|
||||
GET /.well-known/verify/:userId → Verify user exists
|
||||
GET /.well-known/server → Server info
|
||||
```
|
||||
|
||||
### 8.2 Metadata Trust
|
||||
Client may send metadata hints, but server metadata is canonical.
|
||||
Client should refresh with upload response values.
|
||||
### Protected (JWT Required)
|
||||
```
|
||||
GET /api/me → Current user profile
|
||||
|
||||
### 8.3 Caching
|
||||
- `/attachments/:attachmentId`: immutable and long-cache.
|
||||
- `/avatars`, `/icons`, `/banners`: mutable aliases; revalidate frequently.
|
||||
GET /api/channels → List channels
|
||||
POST /api/channels → Create channel
|
||||
GET /api/channels/:id → Get channel
|
||||
|
||||
## 9. AVIF and JPEG XL Policy
|
||||
- MVP default: WebP (best compatibility/perf tradeoff).
|
||||
- AVIF: optional client mode if target platform decode support is verified.
|
||||
- JPEG XL: defer for now due to uneven ecosystem support.
|
||||
GET /api/messages?channelId=X → Get messages
|
||||
POST /api/messages → Send message
|
||||
PUT /api/messages/:id → Edit message
|
||||
DELETE /api/messages/:id → Delete message
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Key Files
|
||||
|
||||
```
|
||||
lib/
|
||||
├── server.dart # Entry point, routes, ObjectBox init
|
||||
├── models.dart # User, Channel, Message entities
|
||||
├── objectbox.g.dart # Generated
|
||||
│
|
||||
├── config/config.dart # Server host, JWT secret, port
|
||||
│
|
||||
└── src/
|
||||
├── auth/
|
||||
│ ├── crypto_service.dart # Keypair, signing, encryption
|
||||
│ └── auth_middleware.dart # JWT verification
|
||||
│
|
||||
├── handlers/
|
||||
│ ├── auth_handlers.dart # register, login, join, me
|
||||
│ ├── public_handlers.dart # .well-known/*
|
||||
│ ├── channel_handlers.dart
|
||||
│ └── message_handlers.dart
|
||||
│
|
||||
└── utils/extensions.dart # Request/Response helpers
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Join Server Flow (Needs Implementation)
|
||||
|
||||
```
|
||||
CLIENT GUEST SERVER HOME SERVER
|
||||
│ │ │
|
||||
│ POST /auth/join │ │
|
||||
│ {userId, homeServer, timestamp, │ │
|
||||
│ signature} │ │
|
||||
│─────────────────────────────────────►│ │
|
||||
│ │ │
|
||||
│ │ GET /.well-known/users/ │
|
||||
│ │ alice │
|
||||
│ │────────────────────────►│
|
||||
│ │ │
|
||||
│ │ {publicKey, ...} │
|
||||
│ │◄────────────────────────│
|
||||
│ │ │
|
||||
│ │ verify signature │
|
||||
│ │ create shadow account │
|
||||
│ │ │
|
||||
│ {token, localId} │ │
|
||||
│◄─────────────────────────────────────│ │
|
||||
```
|
||||
|
||||
The `fetchPublicKeyFromHomeServer()` function in `auth_handlers.dart` needs actual HTTP implementation.
|
||||
|
||||
---
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- **Framework**: Relic 1.0 (Dart web server)
|
||||
- **Database**: ObjectBox
|
||||
- **Auth**: JWT + Ed25519 signatures
|
||||
- **Crypto**: crypton package for Ed25519
|
||||
- **Password**: bcrypt
|
||||
|
||||
---
|
||||
|
||||
## Design Decisions Rationale
|
||||
|
||||
| Decision | Why |
|
||||
|----------|-----|
|
||||
| No identity server | Avoids hosting another service, one server = home |
|
||||
| No server-to-server | Eliminates federation complexity, client handles everything |
|
||||
| Keypair identity | Allows passwordless joins, proves identity cryptographically |
|
||||
| Message author caching | Messages readable even if home server is down |
|
||||
| userId format `user@server` | Simple, familiar, self-documenting |
|
||||
|
||||
---
|
||||
|
||||
## Constraints
|
||||
|
||||
- User CANNOT quit home server (that's where identity lives)
|
||||
- User CANNOT change home server easily (migration = future feature)
|
||||
- Home server MUST be online to join NEW servers
|
||||
- Home server offline = can still use already-joined servers (cached keys)
|
||||
|
||||
---
|
||||
|
||||
## Future Features (Out of Scope for MVP)
|
||||
|
||||
1. **Sticker Packs** - Telegram-style, live on community servers, clients cache
|
||||
2. **Home Server Migration** - Sign migration certificate, chain of trust
|
||||
3. **File Uploads** - With cross-server URLs
|
||||
4. **Message Forwarding** - Embed with link to original
|
||||
5. **Permissions** - Role-based access to channels
|
||||
6. **WebSocket** - Real-time message delivery
|
||||
|
||||
---
|
||||
|
||||
## Config to Change
|
||||
|
||||
```dart
|
||||
// lib/config/config.dart
|
||||
static const String serverHost = 'your-actual-domain.com'; // CHANGE THIS
|
||||
static const String jwtSecret = 'use-env-variable'; // CHANGE THIS
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. Implement HTTP client in `fetchPublicKeyFromHomeServer()`
|
||||
2. Add WebSocket for real-time
|
||||
3. Add profile sync (client pushes to all servers on change)
|
||||
4. Test cross-server join flow
|
||||
5. Add proper error handling and logging
|
||||
@@ -0,0 +1,3 @@
|
||||
## 1.0.0
|
||||
|
||||
- Initial version.
|
||||
@@ -0,0 +1,23 @@
|
||||
:81 {
|
||||
@transform_port_query {
|
||||
query XTransformPort=*
|
||||
}
|
||||
|
||||
handle @transform_port_query {
|
||||
reverse_proxy localhost:{query.XTransformPort} {
|
||||
header_up Host {host}
|
||||
header_up X-Forwarded-For {remote_host}
|
||||
header_up X-Forwarded-Proto {scheme}
|
||||
header_up X-Real-IP {remote_host}
|
||||
}
|
||||
}
|
||||
|
||||
handle {
|
||||
reverse_proxy localhost:3000 {
|
||||
header_up Host {host}
|
||||
header_up X-Forwarded-For {remote_host}
|
||||
header_up X-Forwarded-Proto {scheme}
|
||||
header_up X-Real-IP {remote_host}
|
||||
}
|
||||
}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
# Use latest stable channel SDK.
|
||||
FROM dart:stable AS build
|
||||
|
||||
# Resolve app dependencies.
|
||||
WORKDIR /app
|
||||
COPY pubspec.* ./
|
||||
RUN dart pub get
|
||||
|
||||
# Copy app source code (except anything in .dockerignore) and AOT compile app.
|
||||
COPY . .
|
||||
RUN dart build cli --target bin/server.dart -o output
|
||||
|
||||
# Build minimal serving image from AOT-compiled `/server`
|
||||
# and the pre-built AOT-runtime in the `/runtime/` directory of the base image.
|
||||
FROM scratch
|
||||
COPY --from=build /runtime/ /
|
||||
COPY --from=build /app/output/bundle/ /app/
|
||||
|
||||
# Start server.
|
||||
EXPOSE 8080
|
||||
CMD ["/app/bin/server"]
|
||||
-140
@@ -1,140 +0,0 @@
|
||||
# Reliquary Protocol (MVP v1)
|
||||
|
||||
This document defines the current wire contract used by the server.
|
||||
|
||||
## 1. Identity
|
||||
- `userId`: immutable opaque ID.
|
||||
- `username`: home-server unique, mutable.
|
||||
- `handle`: derived string `@username@home`.
|
||||
- `homeServer`: canonical public base URL.
|
||||
|
||||
## 2. Auth
|
||||
|
||||
### `POST /auth/register`
|
||||
Request:
|
||||
```json
|
||||
{ "username": "alice", "password": "password123" }
|
||||
```
|
||||
Response fields include:
|
||||
- `token`
|
||||
- `userId`
|
||||
- `username`
|
||||
- `handle`
|
||||
- `publicKey`
|
||||
- encrypted key backup fields
|
||||
|
||||
### `POST /auth/login`
|
||||
Request:
|
||||
```json
|
||||
{ "username": "alice", "password": "password123" }
|
||||
```
|
||||
Response includes token + profile/key fields.
|
||||
|
||||
### `POST /auth/join`
|
||||
Request:
|
||||
```json
|
||||
{
|
||||
"userId": "opaque_id",
|
||||
"username": "alice",
|
||||
"homeServer": "https://chat.example.com",
|
||||
"timestamp": 1730000000,
|
||||
"signature": "base64..."
|
||||
}
|
||||
```
|
||||
Signature is over `userId:timestamp`.
|
||||
|
||||
## 3. Message Components
|
||||
|
||||
Each component node must include:
|
||||
- `id` (string)
|
||||
- `type` (string)
|
||||
- `typeVersion` (positive int)
|
||||
- `data` (object)
|
||||
|
||||
Optional:
|
||||
- `fallback`, `modifiers`
|
||||
|
||||
Nested components are supported under `data.children`.
|
||||
|
||||
## 4. Media Item Sources
|
||||
|
||||
Media references support two sources:
|
||||
|
||||
1. Local upload:
|
||||
```json
|
||||
{
|
||||
"source": "local_upload",
|
||||
"attachmentId": "att_...",
|
||||
"status": "uploading|ready|failed"
|
||||
}
|
||||
```
|
||||
|
||||
2. External URL:
|
||||
```json
|
||||
{
|
||||
"source": "external_url",
|
||||
"url": "https://example.com/image.jpg",
|
||||
"status": "ready"
|
||||
}
|
||||
```
|
||||
|
||||
Optional metadata snapshot:
|
||||
- `mimeType`, `sizeBytes`, `width`, `height`, `thumbHash`, `originalFileName`, `caption`.
|
||||
|
||||
## 5. Uploads
|
||||
|
||||
### `POST /api/uploads/init` (auth)
|
||||
Creates pending upload and returns `uploadId` (same ID namespace as attachment ID).
|
||||
|
||||
### `PUT /api/uploads/:uploadId` (auth)
|
||||
Completes upload with file bytes and returns canonical metadata:
|
||||
- `attachmentId`, `url`, `status`, `mimeType`, `sizeBytes`, dimensions, hash.
|
||||
|
||||
### `POST /api/uploads` (auth)
|
||||
Legacy single-step compatibility endpoint.
|
||||
|
||||
### `GET /attachments/:attachmentId`
|
||||
Immutable media URL.
|
||||
|
||||
## 6. Messages
|
||||
|
||||
### `POST /api/messages` (auth)
|
||||
Request includes:
|
||||
- `channelId`
|
||||
- `components` or `componentsJson`
|
||||
- optional `content`
|
||||
|
||||
Server behavior:
|
||||
- validates structure and media refs
|
||||
- normalizes media items
|
||||
- computes searchable `content`
|
||||
- persists `componentsJson`
|
||||
|
||||
### `GET /api/messages?channelId=...`
|
||||
Returns message list with `componentsJson`.
|
||||
|
||||
## 7. Gallery Index
|
||||
|
||||
### `GET /api/channels/:channelId/media` (auth)
|
||||
Returns indexed media rows for grid views.
|
||||
|
||||
Query params:
|
||||
- `limit` (default 50, max 100)
|
||||
- `before` cursor
|
||||
- `kind` (`image|video|file`)
|
||||
|
||||
Each row includes:
|
||||
- linkage (`messageId`, `channelId`, `authorUserId`)
|
||||
- source (`attachmentId` or `externalUrl`)
|
||||
- display metadata (`mimeType`, dimensions, filename, etc.)
|
||||
|
||||
## 8. Gateway
|
||||
|
||||
WebSocket path: `/gateway`
|
||||
|
||||
Message events:
|
||||
- `message.created`
|
||||
- `message.updated`
|
||||
- `message.deleted`
|
||||
|
||||
Upload resolution can cause `message.updated`.
|
||||
@@ -11,7 +11,6 @@ Each user has a **home server** where their identity lives (keypair), and can se
|
||||
- **Client does everything** - syncs, cross-server requests, profile updates
|
||||
|
||||
See [ARCHITECTURE.md](./ARCHITECTURE.md) for full details.
|
||||
See [CLIENT_INTEGRATION.md](./CLIENT_INTEGRATION.md) for client protocol/API integration details.
|
||||
|
||||
## Quick Start
|
||||
|
||||
@@ -44,4 +43,4 @@ lib/
|
||||
|
||||
- **Framework**: Relic 1.0
|
||||
- **Database**: ObjectBox
|
||||
- **Auth**: JWT + ECDSA signatures
|
||||
- **Auth**: JWT + Ed25519 signatures
|
||||
@@ -0,0 +1,30 @@
|
||||
# This file configures the static analysis results for your project (errors,
|
||||
# warnings, and lints).
|
||||
#
|
||||
# This enables the 'recommended' set of lints from `package:lints`.
|
||||
# This set helps identify many issues that may lead to problems when running
|
||||
# or consuming Dart code, and enforces writing Dart using a single, idiomatic
|
||||
# style and format.
|
||||
#
|
||||
# If you want a smaller set of lints you can change this to specify
|
||||
# 'package:lints/core.yaml'. These are just the most critical lints
|
||||
# (the recommended set includes the core lints).
|
||||
# The core lints are also what is used by pub.dev for scoring packages.
|
||||
|
||||
include: package:lints/recommended.yaml
|
||||
|
||||
# Uncomment the following section to specify additional rules.
|
||||
|
||||
# linter:
|
||||
# rules:
|
||||
# - camel_case_types
|
||||
|
||||
# analyzer:
|
||||
# exclude:
|
||||
# - path/to/excluded/files/**
|
||||
|
||||
# For more information about the core and recommended set of lints, see
|
||||
# https://dart.dev/go/core-lints
|
||||
|
||||
# For additional information about configuring this file, see
|
||||
# https://dart.dev/guides/language/analysis-options
|
||||
@@ -0,0 +1,59 @@
|
||||
import 'package:chat_server/config/config.dart';
|
||||
import 'package:chat_server/models.dart';
|
||||
import 'package:chat_server/objectbox.g.dart';
|
||||
import 'package:chat_server/src/auth/auth_middleware.dart';
|
||||
import 'package:chat_server/src/handlers/auth_handlers.dart';
|
||||
import 'package:chat_server/src/handlers/channel_handlers.dart';
|
||||
import 'package:chat_server/src/handlers/debug_handlers.dart';
|
||||
import 'package:chat_server/src/handlers/message_handlers.dart';
|
||||
import 'package:chat_server/src/handlers/user_handlers.dart';
|
||||
import 'package:hotreloader/hotreloader.dart';
|
||||
import 'package:relic/relic.dart';
|
||||
|
||||
// Global ObjectBox boxes
|
||||
final Store store = openStore();
|
||||
final Box<ChatUser> userBox = store.box<ChatUser>();
|
||||
final Box<ChatChannel> channelBox = store.box<ChatChannel>();
|
||||
final Box<ChatMessage> messageBox = store.box<ChatMessage>();
|
||||
|
||||
RelicApp? _currentApp;
|
||||
|
||||
Future<void> startServer() async {
|
||||
await _currentApp?.close();
|
||||
|
||||
_currentApp = RelicApp()
|
||||
// Public routes
|
||||
..post('/login', loginHandler)
|
||||
..post('/register', registerHandler)
|
||||
// Debug routes
|
||||
..get('/debug/users', debugUsersHandler)
|
||||
..get('/debug/messages', debugMessagesHandler)
|
||||
..get('/debug/clear', debugClearHandler)
|
||||
// Protected routes
|
||||
..use('/api', jwtMiddleware)
|
||||
// Channels
|
||||
..get('/api/channels', getChannelsHandler)
|
||||
..post('/api/channels', createChannelHandler)
|
||||
// Messages
|
||||
..get('/api/messages', getMessagesHandler)
|
||||
..post('/api/messages', sendMessageHandler)
|
||||
// Users
|
||||
..get('/api/users', getUsersHandler)
|
||||
// Logging & fallback
|
||||
..use('/', logRequests())
|
||||
..fallback = respondWith((_) => Response.notFound());
|
||||
|
||||
print('🚀 Server starting on http://localhost:${Config.port}');
|
||||
await _currentApp!.serve(port: Config.port);
|
||||
}
|
||||
|
||||
void main() async {
|
||||
await HotReloader.create(
|
||||
onAfterReload: (ctx) async {
|
||||
print('🔄 Changes detected! Restarting server...');
|
||||
await startServer();
|
||||
},
|
||||
);
|
||||
|
||||
await startServer();
|
||||
}
|
||||
Executable
+21
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "new-york",
|
||||
"rsc": true,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "src/app/globals.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
},
|
||||
"iconLibrary": "lucide"
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
{
|
||||
"code": 200,
|
||||
"data": {
|
||||
"description": "Relic is a modern, high-performance web server framework inspired by Shelf and built by the Serverpod team to meet the demanding requirements of modern web applications. Relic provides a robust, type-safe, and efficient foundation for building scalable web services.",
|
||||
"external": {
|
||||
"alternate": {
|
||||
"https://docs.dartrelic.dev/": {
|
||||
"data-rh": "true",
|
||||
"hreflang": "x-default"
|
||||
}
|
||||
},
|
||||
"canonical": {
|
||||
"https://docs.dartrelic.dev/": {
|
||||
"data-rh": "true"
|
||||
}
|
||||
},
|
||||
"icon": {
|
||||
"https://docs.dartrelic.dev/img/favicon.png": {
|
||||
"data-rh": "true"
|
||||
}
|
||||
},
|
||||
"preconnect": {
|
||||
"https://www.google-analytics.com/": {},
|
||||
"https://www.googletagmanager.com/": {}
|
||||
},
|
||||
"prefetch": {
|
||||
"https://docs.dartrelic.dev/assets/js/0ca634c5.c40e5698.js": {},
|
||||
"https://docs.dartrelic.dev/assets/js/17896441.35fb8100.js": {},
|
||||
"https://docs.dartrelic.dev/assets/js/22dd74f7.32b10889.js": {},
|
||||
"https://docs.dartrelic.dev/assets/js/2c10348c.c38ae9dc.js": {},
|
||||
"https://docs.dartrelic.dev/assets/js/2dc195cc.c7b109bb.js": {},
|
||||
"https://docs.dartrelic.dev/assets/js/441e1055.6248ef14.js": {},
|
||||
"https://docs.dartrelic.dev/assets/js/4edc808e.d18ceb40.js": {},
|
||||
"https://docs.dartrelic.dev/assets/js/5b00fbf1.0fa3fba9.js": {},
|
||||
"https://docs.dartrelic.dev/assets/js/5e95c892.753674ac.js": {},
|
||||
"https://docs.dartrelic.dev/assets/js/6783a3b2.0aa1428a.js": {},
|
||||
"https://docs.dartrelic.dev/assets/js/9412f430.c708638d.js": {},
|
||||
"https://docs.dartrelic.dev/assets/js/9576f52c.f029d9c3.js": {},
|
||||
"https://docs.dartrelic.dev/assets/js/a7bd4aaa.b870ce93.js": {},
|
||||
"https://docs.dartrelic.dev/assets/js/a94703ab.055f41b9.js": {},
|
||||
"https://docs.dartrelic.dev/assets/js/a9685393.8fdc4764.js": {},
|
||||
"https://docs.dartrelic.dev/assets/js/aba21aa0.1d722264.js": {},
|
||||
"https://docs.dartrelic.dev/assets/js/aca04f59.28672d9f.js": {},
|
||||
"https://docs.dartrelic.dev/assets/js/b4e2adc5.e5a3b2b2.js": {},
|
||||
"https://docs.dartrelic.dev/assets/js/b685e31b.f5eb8692.js": {},
|
||||
"https://docs.dartrelic.dev/assets/js/be2b5817.53d9f078.js": {},
|
||||
"https://docs.dartrelic.dev/assets/js/c644a7fe.ad3a69e0.js": {},
|
||||
"https://docs.dartrelic.dev/assets/js/c678908c.c170f033.js": {}
|
||||
}
|
||||
},
|
||||
"html": "<html lang=\"en\" dir=\"ltr\" class=\"docs-wrapper plugin-docs plugin-id-default docs-version-1.0.0 docs-doc-page docs-doc-id-index\" data-has-hydrated=\"true\" data-theme=\"light\" data-theme-choice=\"system\" data-rh=\"lang,dir,class,data-has-hydrated\"><head>\n<meta charset=\"UTF-8\">\n<meta name=\"generator\" content=\"Docusaurus v3.9.2\">\n<title>🎯 Introduction | Relic</title><meta data-rh=\"true\" name=\"twitter:card\" content=\"summary_large_image\"><meta data-rh=\"true\" property=\"og:url\" content=\"https://docs.dartrelic.dev/\"><link data-rh=\"true\" rel=\"icon\" href=\"/img/favicon.png\"><link data-rh=\"true\" rel=\"canonical\" href=\"https://docs.dartrelic.dev/\"><link rel=\"preconnect\" href=\"https://www.google-analytics.com\">\n<link rel=\"preconnect\" href=\"https://www.googletagmanager.com\">\n<script async=\"\" src=\"https://www.googletagmanager.com/gtag/js?id=G-0EYLJMP04H\"></script>\n<script>function gtag(){dataLayer.push(arguments)}window.dataLayer=window.dataLayer||[],gtag(\"js\",new Date),gtag(\"config\",\"G-0EYLJMP04H\",{anonymize_ip:!0})</script><link rel=\"stylesheet\" href=\"/assets/css/styles.5c32dd38.css\">\n<script src=\"/assets/js/runtime~main.75ddcb03.js\" defer=\"defer\"></script>\n<script src=\"/assets/js/main.e1e75f7d.js\" defer=\"defer\"></script>\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" data-rh=\"true\"><link rel=\"alternate\" href=\"https://docs.dartrelic.dev/\" hreflang=\"en\" data-rh=\"true\"><link rel=\"alternate\" href=\"https://docs.dartrelic.dev/\" hreflang=\"x-default\" data-rh=\"true\"><meta property=\"og:locale\" content=\"en\" data-rh=\"true\"><meta name=\"docusaurus_locale\" content=\"en\" data-rh=\"true\"><meta name=\"docsearch:language\" content=\"en\" data-rh=\"true\"><meta name=\"docusaurus_version\" content=\"1.0.0\" data-rh=\"true\"><meta name=\"docusaurus_tag\" content=\"docs-default-1.0.0\" data-rh=\"true\"><meta name=\"docsearch:version\" content=\"1.0.0\" data-rh=\"true\"><meta name=\"docsearch:docusaurus_tag\" content=\"docs-default-1.0.0\" data-rh=\"true\"><meta property=\"og:title\" content=\"🎯 Introduction | Relic\" data-rh=\"true\"><meta name=\"description\" content=\"Relic is a modern, high-performance web server framework inspired by Shelf and built by the Serverpod team to meet the demanding requirements of modern web applications. Relic provides a robust, type-safe, and efficient foundation for building scalable web services.\" data-rh=\"true\"><meta property=\"og:description\" content=\"Relic is a modern, high-performance web server framework inspired by Shelf and built by the Serverpod team to meet the demanding requirements of modern web applications. Relic provides a robust, type-safe, and efficient foundation for building scalable web services.\" data-rh=\"true\"><link rel=\"prefetch\" href=\"/assets/js/5e95c892.753674ac.js\"><link rel=\"prefetch\" href=\"/assets/js/aba21aa0.1d722264.js\"><link rel=\"prefetch\" href=\"/assets/js/a7bd4aaa.b870ce93.js\"><link rel=\"prefetch\" href=\"/assets/js/22dd74f7.32b10889.js\"><link rel=\"prefetch\" href=\"/assets/js/a94703ab.055f41b9.js\"><link rel=\"prefetch\" href=\"/assets/js/17896441.35fb8100.js\"><link rel=\"prefetch\" href=\"/assets/js/0ca634c5.c40e5698.js\"><link rel=\"prefetch\" href=\"/assets/js/5e95c892.753674ac.js\"><link rel=\"prefetch\" href=\"/assets/js/aba21aa0.1d722264.js\"><link rel=\"prefetch\" href=\"/assets/js/a7bd4aaa.b870ce93.js\"><link rel=\"prefetch\" href=\"/assets/js/22dd74f7.32b10889.js\"><link rel=\"prefetch\" href=\"/assets/js/a94703ab.055f41b9.js\"><link rel=\"prefetch\" href=\"/assets/js/17896441.35fb8100.js\"><link rel=\"prefetch\" href=\"/assets/js/0ca634c5.c40e5698.js\"><link rel=\"prefetch\" href=\"/assets/js/5e95c892.753674ac.js\"><link rel=\"prefetch\" href=\"/assets/js/aba21aa0.1d722264.js\"><link rel=\"prefetch\" href=\"/assets/js/a7bd4aaa.b870ce93.js\"><link rel=\"prefetch\" href=\"/assets/js/22dd74f7.32b10889.js\"><link rel=\"prefetch\" href=\"/assets/js/a94703ab.055f41b9.js\"><link rel=\"prefetch\" href=\"/assets/js/17896441.35fb8100.js\"><link rel=\"prefetch\" href=\"/assets/js/0ca634c5.c40e5698.js\"><link rel=\"prefetch\" href=\"/assets/js/17896441.35fb8100.js\"><link rel=\"prefetch\" href=\"/assets/js/c644a7fe.ad3a69e0.js\"><link rel=\"prefetch\" href=\"/assets/js/5e95c892.753674ac.js\"><link rel=\"prefetch\" href=\"/assets/js/aba21aa0.1d722264.js\"><link rel=\"prefetch\" href=\"/assets/js/a7bd4aaa.b870ce93.js\"><link rel=\"prefetch\" href=\"/assets/js/22dd74f7.32b10889.js\"><link rel=\"prefetch\" href=\"/assets/js/a94703ab.055f41b9.js\"><link rel=\"prefetch\" href=\"/assets/js/17896441.35fb8100.js\"><link rel=\"prefetch\" href=\"/assets/js/0ca634c5.c40e5698.js\"><link rel=\"prefetch\" href=\"/assets/js/5e95c892.753674ac.js\"><link rel=\"prefetch\" href=\"/assets/js/aba21aa0.1d722264.js\"><link rel=\"prefetch\" href=\"/assets/js/a7bd4aaa.b870ce93.js\"><link rel=\"prefetch\" href=\"/assets/js/22dd74f7Line truncated
|
||||
"metadata": {
|
||||
"description": "Relic is a modern, high-performance web server framework inspired by Shelf and built by the Serverpod team to meet the demanding requirements of modern web applications. Relic provides a robust, type-safe, and efficient foundation for building scalable web services.",
|
||||
"docsearch:docusaurus_tag": "docs-default-1.0.0",
|
||||
"docsearch:language": "en",
|
||||
"docsearch:version": "1.0.0",
|
||||
"docusaurus_locale": "en",
|
||||
"docusaurus_tag": "docs-default-1.0.0",
|
||||
"docusaurus_version": "1.0.0",
|
||||
"generator": "Docusaurus v3.9.2",
|
||||
"lang": "en",
|
||||
"og:description": "Relic is a modern, high-performance web server framework inspired by Shelf and built by the Serverpod team to meet the demanding requirements of modern web applications. Relic provides a robust, type-safe, and efficient foundation for building scalable web services.",
|
||||
"og:locale": "en",
|
||||
"og:title": "🎯 Introduction | Relic",
|
||||
"og:url": "https://docs.dartrelic.dev/",
|
||||
"twitter:card": "summary_large_image",
|
||||
"viewport": "width=device-width, initial-scale=1.0"
|
||||
},
|
||||
"title": "🎯 Introduction | Relic",
|
||||
"url": "https://docs.dartrelic.dev/",
|
||||
"usage": {
|
||||
"tokens": 14117
|
||||
}
|
||||
},
|
||||
"meta": {
|
||||
"usage": {
|
||||
"tokens": 14117
|
||||
}
|
||||
},
|
||||
"status": 20000
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
{
|
||||
"code": 200,
|
||||
"data": {
|
||||
"description": "Once you have Dart installed, it only takes a few lines of code to set up your Relic server. These are the steps you need to take to get a simple Hello world server up and running.",
|
||||
"external": {
|
||||
"alternate": {
|
||||
"https://docs.dartrelic.dev/getting-started/hello-world-example": {
|
||||
"data-rh": "true",
|
||||
"hreflang": "x-default"
|
||||
}
|
||||
},
|
||||
"canonical": {
|
||||
"https://docs.dartrelic.dev/getting-started/hello-world-example": {
|
||||
"data-rh": "true"
|
||||
}
|
||||
},
|
||||
"icon": {
|
||||
"https://docs.dartrelic.dev/img/favicon.png": {
|
||||
"data-rh": "true"
|
||||
}
|
||||
},
|
||||
"preconnect": {
|
||||
"https://www.google-analytics.com/": {},
|
||||
"https://www.googletagmanager.com/": {}
|
||||
},
|
||||
"prefetch": {
|
||||
"https://docs.dartrelic.dev/assets/js/0ca634c5.c40e5698.js": {},
|
||||
"https://docs.dartrelic.dev/assets/js/17896441.35fb8100.js": {},
|
||||
"https://docs.dartrelic.dev/assets/js/22dd74f7.32b10889.js": {},
|
||||
"https://docs.dartrelic.dev/assets/js/2c10348c.c38ae9dc.js": {},
|
||||
"https://docs.dartrelic.dev/assets/js/2dc195cc.c7b109bb.js": {},
|
||||
"https://docs.dartrelic.dev/assets/js/441e1055.6248ef14.js": {},
|
||||
"https://docs.dartrelic.dev/assets/js/4edc808e.d18ceb40.js": {},
|
||||
"https://docs.dartrelic.dev/assets/js/5b00fbf1.0fa3fba9.js": {},
|
||||
"https://docs.dartrelic.dev/assets/js/5e95c892.753674ac.js": {},
|
||||
"https://docs.dartrelic.dev/assets/js/6783a3b2.0aa1428a.js": {},
|
||||
"https://docs.dartrelic.dev/assets/js/9412f430.c708638d.js": {},
|
||||
"https://docs.dartrelic.dev/assets/js/9576f52c.f029d9c3.js": {},
|
||||
"https://docs.dartrelic.dev/assets/js/a7bd4aaa.b870ce93.js": {},
|
||||
"https://docs.dartrelic.dev/assets/js/a94703ab.055f41b9.js": {},
|
||||
"https://docs.dartrelic.dev/assets/js/a9685393.8fdc4764.js": {},
|
||||
"https://docs.dartrelic.dev/assets/js/aba21aa0.1d722264.js": {},
|
||||
"https://docs.dartrelic.dev/assets/js/aca04f59.28672d9f.js": {},
|
||||
"https://docs.dartrelic.dev/assets/js/b4e2adc5.e5a3b2b2.js": {},
|
||||
"https://docs.dartrelic.dev/assets/js/b685e31b.f5eb8692.js": {},
|
||||
"https://docs.dartrelic.dev/assets/js/be2b5817.53d9f078.js": {},
|
||||
"https://docs.dartrelic.dev/assets/js/c644a7fe.ad3a69e0.js": {},
|
||||
"https://docs.dartrelic.dev/assets/js/c678908c.c170f033.js": {},
|
||||
"https://docs.dartrelic.dev/assets/js/f22cf6c3.3fb896a1.js": {}
|
||||
}
|
||||
},
|
||||
"html": "<html lang=\"en\" dir=\"ltr\" class=\"docs-wrapper plugin-docs plugin-id-default docs-version-1.0.0 docs-doc-page docs-doc-id-getting-started/hello-world-example\" data-has-hydrated=\"true\" data-theme=\"light\" data-theme-choice=\"system\" data-rh=\"lang,dir,class,data-has-hydrated\"><head>\n<meta charset=\"UTF-8\">\n<meta name=\"generator\" content=\"Docusaurus v3.9.2\">\n<title>Hello world | Relic</title><meta data-rh=\"true\" name=\"twitter:card\" content=\"summary_large_image\"><meta data-rh=\"true\" property=\"og:url\" content=\"https://docs.dartrelic.dev/getting-started/hello-world-example\"><meta data-rh=\"true\" property=\"og:locale\" content=\"en\"><meta data-rh=\"true\" name=\"docusaurus_locale\" content=\"en\"><meta data-rh=\"true\" name=\"docsearch:language\" content=\"en\"><link data-rh=\"true\" rel=\"icon\" href=\"/img/favicon.png\"><link data-rh=\"true\" rel=\"canonical\" href=\"https://docs.dartrelic.dev/getting-started/hello-world-example\"><link data-rh=\"true\" rel=\"alternate\" href=\"https://docs.dartrelic.dev/getting-started/hello-world-example\" hreflang=\"en\"><link data-rh=\"true\" rel=\"alternate\" href=\"https://docs.dartrelic.dev/getting-started/hello-world-example\" hreflang=\"x-default\"><link rel=\"preconnect\" href=\"https://www.google-analytics.com\">\n<link rel=\"preconnect\" href=\"https://www.googletagmanager.com\">\n<script async=\"\" src=\"https://www.googletagmanager.com/gtag/js?id=G-0EYLJMP04H\"></script>\n<script>function gtag(){dataLayer.push(arguments)}window.dataLayer=window.dataLayer||[],gtag(\"js\",new Date),gtag(\"config\",\"G-0EYLJMP04H\",{anonymize_ip:!0})</script><link rel=\"stylesheet\" href=\"/assets/css/styles.5c32dd38.css\">\n<script src=\"/assets/js/runtime~main.75ddcb03.js\" defer=\"defer\"></script>\n<script src=\"/assets/js/main.e1e75f7d.js\" defer=\"defer\"></script>\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" data-rh=\"true\"><meta name=\"docusaurus_version\" content=\"1.0.0\" data-rh=\"true\"><meta name=\"docusaurus_tag\" content=\"docs-default-1.0.0\" data-rh=\"true\"><meta name=\"docsearch:version\" content=\"1.0.0\" data-rh=\"true\"><meta name=\"docsearch:docusaurus_tag\" content=\"docs-default-1.0.0\" data-rh=\"true\"><meta property=\"og:title\" content=\"Hello world | Relic\" data-rh=\"true\"><meta name=\"description\" content=\"Once you have Dart installed, it only takes a few lines of code to set up your Relic server. These are the steps you need to take to get a simple Hello world server up and running.\" data-rh=\"true\"><meta property=\"og:description\" content=\"Once you have Dart installed, it only takes a few lines of code to set up your Relic server. These are the steps you need to take to get a simple Hello world server up and running.\" data-rh=\"true\"><link rel=\"prefetch\" href=\"/assets/js/5e95c892.753674ac.js\"><link rel=\"prefetch\" href=\"/assets/js/aba21aa0.1d722264.js\"><link rel=\"prefetch\" href=\"/assets/js/a7bd4aaa.b870ce93.js\"><link rel=\"prefetch\" href=\"/assets/js/22dd74f7.32b10889.js\"><link rel=\"prefetch\" href=\"/assets/js/a94703ab.055f41b9.js\"><link rel=\"prefetch\" href=\"/assets/js/17896441.35fb8100.js\"><link rel=\"prefetch\" href=\"/assets/js/0ca634c5.c40e5698.js\"><link rel=\"prefetch\" href=\"/assets/js/5e95c892.753674ac.js\"><link rel=\"prefetch\" href=\"/assets/js/aba21aa0.1d722264.js\"><link rel=\"prefetch\" href=\"/assets/js/a7bd4aaa.b870ce93.js\"><link rel=\"prefetch\" href=\"/assets/js/22dd74f7.32b10889.js\"><link rel=\"prefetch\" href=\"/assets/js/a94703ab.055f41b9.js\"><link rel=\"prefetch\" href=\"/assets/js/17896441.35fb8100.js\"><link rel=\"prefetch\" href=\"/assets/js/0ca634c5.c40e5698.js\"><link rel=\"prefetch\" href=\"/assets/js/5e95c892.753674ac.js\"><link rel=\"prefetch\" href=\"/assets/js/aba21aa0.1d722264.js\"><link rel=\"prefetch\" href=\"/assets/js/a7bd4aaa.b870ce93.js\"><link rel=\"prefetch\" href=\"/assets/js/22dd74f7.32b10889.js\"><link rel=\"prefetch\" href=\"/assets/js/a94703ab.055f41b9.js\"><link rel=\"prefetch\" href=\"/assets/js/17896441.35fb8100.js\"><link rel=\"prefetch\" href=\"/assets/js/0ca634c5.c40e5698.js\"><link rel=\"prefetch\" href=\"/assets/js/17896441.35fb8100.js\"><link rel=\"prefetch\" href=\"/assets/js/c678908c.c170f033.js\"><link rel=\"prefetch\" href=\"/assets/js/5e95c892.753674ac.js\"><link rel=\"prefetch\" href=\"/assets/js/aba21aa0.1d722264.js\"><link rel=\"prefetch\" href=\"/assets/js/a7bd4aaa.b870ce93.js\"><link rel=\"prefetch\" href=\"/assets/js/22dd74f7.32b10889.js\"><link rel=\"prefetch\" href=\"/assets/js/a94703ab.055f41b9.js\"><link rel=\"prefetch\" href=\"/assets/js/17896441.35fb8100.js\"><link rel=\"prefetch\" href=\"/assets/js/0ca634c5.c40e5698.js\"><link rel=\"prefetch\" href=\"/assets/js/5e95c892.753674ac.js\"><link rel=\"prefetch\" href=\"/assets/js/aba21aa0.1d722264.js\"><link rel=\"prefetch\" href=\"/assets/js/a7bd4aaa.b870ce93.js\"><link rel=\"prefetch\" href=\"/assets/js/22dd74f7.32b10889.js\"Line truncated
|
||||
"metadata": {
|
||||
"description": "Once you have Dart installed, it only takes a few lines of code to set up your Relic server. These are the steps you need to take to get a simple Hello world server up and running.",
|
||||
"docsearch:docusaurus_tag": "docs-default-1.0.0",
|
||||
"docsearch:language": "en",
|
||||
"docsearch:version": "1.0.0",
|
||||
"docusaurus_locale": "en",
|
||||
"docusaurus_tag": "docs-default-1.0.0",
|
||||
"docusaurus_version": "1.0.0",
|
||||
"generator": "Docusaurus v3.9.2",
|
||||
"lang": "en",
|
||||
"og:description": "Once you have Dart installed, it only takes a few lines of code to set up your Relic server. These are the steps you need to take to get a simple Hello world server up and running.",
|
||||
"og:locale": "en",
|
||||
"og:title": "Hello world | Relic",
|
||||
"og:url": "https://docs.dartrelic.dev/getting-started/hello-world-example",
|
||||
"twitter:card": "summary_large_image",
|
||||
"viewport": "width=device-width, initial-scale=1.0"
|
||||
},
|
||||
"publishedTime": "Wed, 18 Feb 2026 09:05:14 GMT",
|
||||
"title": "Hello world | Relic",
|
||||
"url": "https://docs.dartrelic.dev/getting-started/hello-world-example",
|
||||
"usage": {
|
||||
"tokens": 21616
|
||||
}
|
||||
},
|
||||
"meta": {
|
||||
"usage": {
|
||||
"tokens": 21616
|
||||
}
|
||||
},
|
||||
"status": 20000
|
||||
}
|
||||
Executable
BIN
Binary file not shown.
@@ -0,0 +1,90 @@
|
||||
▲ Next.js 16.1.3 (Turbopack)
|
||||
- Local: http://localhost:3000
|
||||
- Network: http://21.0.12.197:3000
|
||||
- Environments: .env
|
||||
|
||||
✓ Starting...
|
||||
Attention: Next.js now collects completely anonymous telemetry regarding usage.
|
||||
This information is used to shape Next.js' roadmap and prioritize features.
|
||||
You can learn more, including how to opt-out if you'd not like to participate in this anonymous program, by visiting the following URL:
|
||||
https://nextjs.org/telemetry
|
||||
|
||||
✓ Ready in 511ms
|
||||
GET / 200 in 2.1s (compile: 2.0s, render: 130ms)
|
||||
GET / 200 in 22ms (compile: 2ms, render: 20ms)
|
||||
GET / 200 in 36ms (compile: 8ms, render: 28ms)
|
||||
GET / 200 in 34ms (compile: 3ms, render: 31ms)
|
||||
✓ Compiled in 190ms
|
||||
Error: ENOENT: no such file or directory, scandir '/home/z/my-project/src/app'
|
||||
at ignore-listed frames {
|
||||
errno: -2,
|
||||
code: 'ENOENT',
|
||||
syscall: 'scandir',
|
||||
path: '/home/z/my-project/src/app'
|
||||
}
|
||||
⨯ unhandledRejection: Error: ENOENT: no such file or directory, scandir '/home/z/my-project/src/app'
|
||||
at ignore-listed frames {
|
||||
errno: -2,
|
||||
code: 'ENOENT',
|
||||
syscall: 'scandir',
|
||||
path: '/home/z/my-project/src/app'
|
||||
}
|
||||
⨯ unhandledRejection: Error: ENOENT: no such file or directory, scandir '/home/z/my-project/src/app'
|
||||
at ignore-listed frames {
|
||||
errno: -2,
|
||||
code: 'ENOENT',
|
||||
syscall: 'scandir',
|
||||
path: '/home/z/my-project/src/app'
|
||||
}
|
||||
Error: ENOENT: no such file or directory, scandir '/home/z/my-project/src/app'
|
||||
at ignore-listed frames {
|
||||
errno: -2,
|
||||
code: 'ENOENT',
|
||||
syscall: 'scandir',
|
||||
path: '/home/z/my-project/src/app'
|
||||
}
|
||||
Error: ENOENT: no such file or directory, scandir '/home/z/my-project/src/app'
|
||||
at ignore-listed frames {
|
||||
errno: -2,
|
||||
code: 'ENOENT',
|
||||
syscall: 'scandir',
|
||||
path: '/home/z/my-project/src/app'
|
||||
}
|
||||
Error: ENOENT: no such file or directory, scandir '/home/z/my-project/src/app'
|
||||
at ignore-listed frames {
|
||||
errno: -2,
|
||||
code: 'ENOENT',
|
||||
syscall: 'scandir',
|
||||
path: '/home/z/my-project/src/app'
|
||||
}
|
||||
Error: ENOENT: no such file or directory, scandir '/home/z/my-project/src/app'
|
||||
at ignore-listed frames {
|
||||
errno: -2,
|
||||
code: 'ENOENT',
|
||||
syscall: 'scandir',
|
||||
path: '/home/z/my-project/src/app'
|
||||
}
|
||||
Error: ENOENT: no such file or directory, scandir '/home/z/my-project/src/app'
|
||||
at ignore-listed frames {
|
||||
errno: -2,
|
||||
code: 'ENOENT',
|
||||
syscall: 'scandir',
|
||||
path: '/home/z/my-project/src/app'
|
||||
}
|
||||
Error: ENOENT: no such file or directory, scandir '/home/z/my-project/src/app'
|
||||
at ignore-listed frames {
|
||||
errno: -2,
|
||||
code: 'ENOENT',
|
||||
syscall: 'scandir',
|
||||
path: '/home/z/my-project/src/app'
|
||||
}
|
||||
⚠ Cross origin request detected from preview-chat-f87de741-7304-4acf-b0b7-879fb722e7ed.space.z.ai to /_next/* resource. In a future major version of Next.js, you will need to explicitly configure "allowedDevOrigins" in next.config to allow this.
|
||||
Read more: https://nextjs.org/docs/app/api-reference/config/next-config-js/allowedDevOrigins
|
||||
⚠ Found a change in next.config.ts. Restarting the server to apply the changes...
|
||||
▲ Next.js 16.1.3 (Turbopack)
|
||||
- Local: http://localhost:3000
|
||||
- Network: http://21.0.12.197:3000
|
||||
- Environments: .env
|
||||
|
||||
✓ Starting...
|
||||
✓ Ready in 509ms
|
||||
Executable
+1
@@ -0,0 +1 @@
|
||||
Here are all the generated files.
|
||||
Regular → Executable
File renamed without changes.
Regular → Executable
File renamed without changes.
Executable
+50
@@ -0,0 +1,50 @@
|
||||
import nextCoreWebVitals from "eslint-config-next/core-web-vitals";
|
||||
import nextTypescript from "eslint-config-next/typescript";
|
||||
import { dirname } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
const eslintConfig = [...nextCoreWebVitals, ...nextTypescript, {
|
||||
rules: {
|
||||
// TypeScript rules
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-unused-vars": "off",
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
"@typescript-eslint/prefer-as-const": "off",
|
||||
"@typescript-eslint/no-unused-disable-directive": "off",
|
||||
|
||||
// React rules
|
||||
"react-hooks/exhaustive-deps": "off",
|
||||
"react-hooks/purity": "off",
|
||||
"react/no-unescaped-entities": "off",
|
||||
"react/display-name": "off",
|
||||
"react/prop-types": "off",
|
||||
"react-compiler/react-compiler": "off",
|
||||
|
||||
// Next.js rules
|
||||
"@next/next/no-img-element": "off",
|
||||
"@next/next/no-html-link-for-pages": "off",
|
||||
|
||||
// General JavaScript rules
|
||||
"prefer-const": "off",
|
||||
"no-unused-vars": "off",
|
||||
"no-console": "off",
|
||||
"no-debugger": "off",
|
||||
"no-empty": "off",
|
||||
"no-irregular-whitespace": "off",
|
||||
"no-case-declarations": "off",
|
||||
"no-fallthrough": "off",
|
||||
"no-mixed-spaces-and-tabs": "off",
|
||||
"no-redeclare": "off",
|
||||
"no-undef": "off",
|
||||
"no-unreachable": "off",
|
||||
"no-useless-escape": "off",
|
||||
},
|
||||
}, {
|
||||
ignores: ["node_modules/**", ".next/**", "out/**", "build/**", "next-env.d.ts", "examples/**", "skills"]
|
||||
}];
|
||||
|
||||
export default eslintConfig;
|
||||
Executable
+196
@@ -0,0 +1,196 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { io } from 'socket.io-client';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
|
||||
type User = {
|
||||
id: string;
|
||||
username: string;
|
||||
}
|
||||
|
||||
type Message = {
|
||||
id: string;
|
||||
username: string;
|
||||
content: string;
|
||||
timestamp: Date | string;
|
||||
type: 'user' | 'system';
|
||||
}
|
||||
|
||||
export default function SocketDemo() {
|
||||
const [messages, setMessages] = useState<Message[]>([]);
|
||||
const [inputMessage, setInputMessage] = useState('');
|
||||
const [username, setUsername] = useState('');
|
||||
const [isUsernameSet, setIsUsernameSet] = useState(false);
|
||||
const [socket, setSocket] = useState<any>(null);
|
||||
const [isConnected, setIsConnected] = useState(false);
|
||||
const [users, setUsers] = useState<User[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
// Connect to websocket server
|
||||
// Never use PORT in the URL, alyways use XTransformPort
|
||||
// DO NOT change the path, it is used by Caddy to forward the request to the correct port
|
||||
const socketInstance = io('/?XTransformPort=3003', {
|
||||
transports: ['websocket', 'polling'],
|
||||
forceNew: true,
|
||||
reconnection: true,
|
||||
reconnectionAttempts: 5,
|
||||
reconnectionDelay: 1000,
|
||||
timeout: 10000
|
||||
})
|
||||
|
||||
setSocket(socketInstance);
|
||||
|
||||
socketInstance.on('connect', () => {
|
||||
setIsConnected(true);
|
||||
});
|
||||
|
||||
socketInstance.on('disconnect', () => {
|
||||
setIsConnected(false);
|
||||
});
|
||||
|
||||
socketInstance.on('message', (msg: Message) => {
|
||||
setMessages(prev => [...prev, msg]);
|
||||
});
|
||||
|
||||
socketInstance.on('user-joined', (data: { user: User; message: Message }) => {
|
||||
setMessages(prev => [...prev, data.message]);
|
||||
setUsers(prev => {
|
||||
if (!prev.find(u => u.id === data.user.id)) {
|
||||
return [...prev, data.user];
|
||||
}
|
||||
return prev;
|
||||
});
|
||||
});
|
||||
|
||||
socketInstance.on('user-left', (data: { user: User; message: Message }) => {
|
||||
setMessages(prev => [...prev, data.message]);
|
||||
setUsers(prev => prev.filter(u => u.id !== data.user.id));
|
||||
});
|
||||
|
||||
socketInstance.on('users-list', (data: { users: User[] }) => {
|
||||
setUsers(data.users);
|
||||
});
|
||||
|
||||
return () => {
|
||||
socketInstance.disconnect();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleJoin = () => {
|
||||
if (socket && username.trim() && isConnected) {
|
||||
socket.emit('join', { username: username.trim() });
|
||||
setIsUsernameSet(true);
|
||||
}
|
||||
};
|
||||
|
||||
const sendMessage = () => {
|
||||
if (socket && inputMessage.trim() && username.trim()) {
|
||||
socket.emit('message', {
|
||||
content: inputMessage.trim(),
|
||||
username: username.trim()
|
||||
});
|
||||
setInputMessage('');
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyPress = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
sendMessage();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container mx-auto p-4 max-w-2xl">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center justify-between">
|
||||
WebSocket Demo
|
||||
<span className={`text-sm px-2 py-1 rounded ${isConnected ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'}`}>
|
||||
{isConnected ? 'Connected' : 'Disconnected'}
|
||||
</span>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{!isUsernameSet ? (
|
||||
<div className="space-y-2">
|
||||
<Input
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
onKeyPress={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleJoin();
|
||||
}
|
||||
}}
|
||||
placeholder="Enter your username..."
|
||||
disabled={!isConnected}
|
||||
className="flex-1"
|
||||
/>
|
||||
<Button
|
||||
onClick={handleJoin}
|
||||
disabled={!isConnected || !username.trim()}
|
||||
className="w-full"
|
||||
>
|
||||
Join Chat
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<ScrollArea className="h-80 w-full border rounded-md p-4">
|
||||
<div className="space-y-2">
|
||||
{messages.length === 0 ? (
|
||||
<p className="text-gray-500 text-center">No messages yet</p>
|
||||
) : (
|
||||
messages.map((msg) => (
|
||||
<div key={msg.id} className="border-b pb-2 last:border-b-0">
|
||||
<div className="flex justify-between items-start">
|
||||
<div className="flex-1">
|
||||
<p className={`text-sm font-medium ${msg.type === 'system'
|
||||
? 'text-blue-600 italic'
|
||||
: 'text-gray-700'
|
||||
}`}>
|
||||
{msg.username}
|
||||
</p>
|
||||
<p className={`${msg.type === 'system'
|
||||
? 'text-blue-500 italic'
|
||||
: 'text-gray-900'
|
||||
}`}>
|
||||
{msg.content}
|
||||
</p>
|
||||
</div>
|
||||
<span className="text-xs text-gray-500">
|
||||
{new Date(msg.timestamp).toLocaleTimeString()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
<div className="flex space-x-2">
|
||||
<Input
|
||||
value={inputMessage}
|
||||
onChange={(e) => setInputMessage(e.target.value)}
|
||||
onKeyPress={handleKeyPress}
|
||||
placeholder="Type a message..."
|
||||
disabled={!isConnected}
|
||||
className="flex-1"
|
||||
/>
|
||||
<Button
|
||||
onClick={sendMessage}
|
||||
disabled={!isConnected || !inputMessage.trim()}
|
||||
>
|
||||
Send
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Executable
+138
@@ -0,0 +1,138 @@
|
||||
import { createServer } from 'http'
|
||||
import { Server } from 'socket.io'
|
||||
|
||||
const httpServer = createServer()
|
||||
const io = new Server(httpServer, {
|
||||
// DO NOT change the path, it is used by Caddy to forward the request to the correct port
|
||||
path: '/',
|
||||
cors: {
|
||||
origin: "*",
|
||||
methods: ["GET", "POST"]
|
||||
},
|
||||
pingTimeout: 60000,
|
||||
pingInterval: 25000,
|
||||
})
|
||||
|
||||
interface User {
|
||||
id: string
|
||||
username: string
|
||||
}
|
||||
|
||||
interface Message {
|
||||
id: string
|
||||
username: string
|
||||
content: string
|
||||
timestamp: Date
|
||||
type: 'user' | 'system'
|
||||
}
|
||||
|
||||
const users = new Map<string, User>()
|
||||
|
||||
const generateMessageId = () => Math.random().toString(36).substr(2, 9)
|
||||
|
||||
const createSystemMessage = (content: string): Message => ({
|
||||
id: generateMessageId(),
|
||||
username: 'System',
|
||||
content,
|
||||
timestamp: new Date(),
|
||||
type: 'system'
|
||||
})
|
||||
|
||||
const createUserMessage = (username: string, content: string): Message => ({
|
||||
id: generateMessageId(),
|
||||
username,
|
||||
content,
|
||||
timestamp: new Date(),
|
||||
type: 'user'
|
||||
})
|
||||
|
||||
io.on('connection', (socket) => {
|
||||
console.log(`User connected: ${socket.id}`)
|
||||
|
||||
// Add test event handler
|
||||
socket.on('test', (data) => {
|
||||
console.log('Received test message:', data)
|
||||
socket.emit('test-response', {
|
||||
message: 'Server received test message',
|
||||
data: data,
|
||||
timestamp: new Date().toISOString()
|
||||
})
|
||||
})
|
||||
|
||||
socket.on('join', (data: { username: string }) => {
|
||||
const { username } = data
|
||||
|
||||
// Create user object
|
||||
const user: User = {
|
||||
id: socket.id,
|
||||
username
|
||||
}
|
||||
|
||||
// Add to user list
|
||||
users.set(socket.id, user)
|
||||
|
||||
// Send join message to all users
|
||||
const joinMessage = createSystemMessage(`${username} joined the chat room`)
|
||||
io.emit('user-joined', { user, message: joinMessage })
|
||||
|
||||
// Send current user list to new user
|
||||
const usersList = Array.from(users.values())
|
||||
socket.emit('users-list', { users: usersList })
|
||||
|
||||
console.log(`${username} joined the chat room, current online users: ${users.size}`)
|
||||
})
|
||||
|
||||
socket.on('message', (data: { content: string; username: string }) => {
|
||||
const { content, username } = data
|
||||
const user = users.get(socket.id)
|
||||
|
||||
if (user && user.username === username) {
|
||||
const message = createUserMessage(username, content)
|
||||
io.emit('message', message)
|
||||
console.log(`${username}: ${content}`)
|
||||
}
|
||||
})
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
const user = users.get(socket.id)
|
||||
|
||||
if (user) {
|
||||
// Remove from user list
|
||||
users.delete(socket.id)
|
||||
|
||||
// Send leave message to all users
|
||||
const leaveMessage = createSystemMessage(`${user.username} left the chat room`)
|
||||
io.emit('user-left', { user: { id: socket.id, username: user.username }, message: leaveMessage })
|
||||
|
||||
console.log(`${user.username} left the chat room, current online users: ${users.size}`)
|
||||
} else {
|
||||
console.log(`User disconnected: ${socket.id}`)
|
||||
}
|
||||
})
|
||||
|
||||
socket.on('error', (error) => {
|
||||
console.error(`Socket error (${socket.id}):`, error)
|
||||
})
|
||||
})
|
||||
|
||||
const PORT = 3003
|
||||
httpServer.listen(PORT, () => {
|
||||
console.log(`WebSocket server running on port ${PORT}`)
|
||||
})
|
||||
|
||||
// Graceful shutdown
|
||||
process.on('SIGTERM', () => {
|
||||
console.log('Received SIGTERM signal, shutting down server...')
|
||||
httpServer.close(() => {
|
||||
console.log('WebSocket server closed')
|
||||
process.exit(0)
|
||||
})
|
||||
})
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
console.log('Received SIGINT signal, shutting down server...')
|
||||
httpServer.close(() => {
|
||||
console.log('WebSocket server closed')
|
||||
process.exit(0)
|
||||
})
|
||||
})
|
||||
+11
-159
@@ -1,162 +1,14 @@
|
||||
import 'dart:io';
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:yaml/yaml.dart';
|
||||
|
||||
class Config {
|
||||
static late String bindHost;
|
||||
static late int bindPort;
|
||||
static late String publicBaseUrl;
|
||||
static late String serverName;
|
||||
static late String jwtSecret;
|
||||
static late bool configured;
|
||||
static late bool setupMode;
|
||||
static late int bootstrapAdminLimit;
|
||||
static late bool devDbTools;
|
||||
|
||||
// Server settings
|
||||
static const int port = 9870;
|
||||
|
||||
// IMPORTANT: Set this to your actual server host/domain
|
||||
// Used for generating userId: "username@serverHost"
|
||||
static const String serverHost = 'localhost:9870';
|
||||
static const String serverName = 'Chat Server';
|
||||
|
||||
// JWT settings
|
||||
// TODO: Move to environment variable in production
|
||||
static const String jwtSecret = 'your_super_secret_key_change_in_production';
|
||||
static const Duration tokenExpiry = Duration(days: 7);
|
||||
|
||||
static void initialize() {
|
||||
final configFile = File('config.yaml');
|
||||
|
||||
if (!configFile.existsSync()) {
|
||||
final generatedSecret = _generateJwtSecret();
|
||||
configFile.writeAsStringSync(_defaultConfig(generatedSecret));
|
||||
print('✅ Generated config.yaml (first run).');
|
||||
print('⚠️ Review server.public_base_url and security.jwt_secret before production use.');
|
||||
}
|
||||
|
||||
final yamlString = configFile.readAsStringSync();
|
||||
final yamlMap = loadYaml(yamlString) as YamlMap;
|
||||
|
||||
final serverMap = yamlMap['server'] as YamlMap?;
|
||||
final securityMap = yamlMap['security'] as YamlMap?;
|
||||
|
||||
serverName = (serverMap?['name'] as String?)?.trim() ?? '';
|
||||
bindHost = (serverMap?['bind_host'] as String?)?.trim() ?? (serverMap?['host'] as String?)?.trim() ?? '127.0.0.1';
|
||||
bindPort = (serverMap?['bind_port'] as int?) ?? (serverMap?['port'] as int?) ?? 9870;
|
||||
final rawPublicBaseUrl =
|
||||
(serverMap?['public_base_url'] as String?)?.trim() ?? _legacyPublicBaseUrlFromHost((serverMap?['host'] as String?)?.trim());
|
||||
publicBaseUrl = _normalizePublicBaseUrl(rawPublicBaseUrl) ?? '';
|
||||
configured = (serverMap?['configured'] as bool?) ?? false;
|
||||
setupMode = (serverMap?['first_setup'] as bool?) ?? false;
|
||||
bootstrapAdminLimit = (serverMap?['bootstrap_admin_limit'] as int?) ?? 1;
|
||||
devDbTools = (serverMap?['dev_db_tools'] as bool?) ?? false;
|
||||
|
||||
jwtSecret = (securityMap?['jwt_secret'] as String?)?.trim() ?? '';
|
||||
|
||||
if (serverName.isEmpty) {
|
||||
throw StateError('config.yaml: server.name is required.');
|
||||
}
|
||||
if (bindHost.isEmpty) {
|
||||
throw StateError('config.yaml: server.bind_host is required.');
|
||||
}
|
||||
if (bindPort <= 0 || bindPort > 65535) {
|
||||
throw StateError('config.yaml: server.bind_port must be between 1 and 65535.');
|
||||
}
|
||||
if (publicBaseUrl.isEmpty) {
|
||||
throw StateError(
|
||||
'config.yaml: server.public_base_url is required (example: https://chat.example.com).',
|
||||
);
|
||||
}
|
||||
if (jwtSecret.isEmpty) {
|
||||
throw StateError('config.yaml: security.jwt_secret is required.');
|
||||
}
|
||||
if (!configured) {
|
||||
throw StateError(
|
||||
'Server setup is not complete.\n'
|
||||
'Edit config.yaml and set:\n'
|
||||
' server.configured: true\n'
|
||||
'After reviewing server.public_base_url and security.jwt_secret.',
|
||||
);
|
||||
}
|
||||
if (bootstrapAdminLimit < 0) {
|
||||
throw StateError('config.yaml: server.bootstrap_admin_limit must be >= 0.');
|
||||
}
|
||||
|
||||
print(
|
||||
'🚀 Server Config Loaded: $serverName (bind=$bindHost:$bindPort, public=$publicBaseUrl, configured=$configured, setupMode=$setupMode, bootstrapAdminLimit=$bootstrapAdminLimit, devDbTools=$devDbTools)',
|
||||
);
|
||||
}
|
||||
|
||||
static String _generateJwtSecret() {
|
||||
final random = Random.secure();
|
||||
final bytes = List<int>.generate(32, (_) => random.nextInt(256));
|
||||
return base64UrlEncode(bytes);
|
||||
}
|
||||
|
||||
static String _defaultConfig(String generatedSecret) => '''
|
||||
# Reliquary Server Configuration
|
||||
server:
|
||||
# Display name shown in clients
|
||||
name: "Reliquary Server"
|
||||
|
||||
# Local bind address for the HTTP server
|
||||
bind_host: "127.0.0.1"
|
||||
|
||||
# Local bind port for the HTTP server
|
||||
bind_port: 9870
|
||||
|
||||
# Public base URL used for identity, handles, and cross-server references
|
||||
# Dev example: http://localhost:9870
|
||||
# Prod example: https://chat.example.com
|
||||
public_base_url: "http://localhost:9870"
|
||||
|
||||
# Set to true after reviewing this file; server refuses to start while false
|
||||
configured: false
|
||||
|
||||
# First local registration becomes admin
|
||||
first_setup: true
|
||||
|
||||
# Number of local registrations auto-promoted to admin while first_setup=true
|
||||
bootstrap_admin_limit: 1
|
||||
|
||||
# Testing-only DB admin/debug endpoints
|
||||
dev_db_tools: true
|
||||
|
||||
security:
|
||||
# Auto-generated on first run; replace for production
|
||||
jwt_secret: "$generatedSecret"
|
||||
''';
|
||||
|
||||
static String get publicHost {
|
||||
final uri = Uri.tryParse(publicBaseUrl);
|
||||
if (uri == null || uri.host.isEmpty) {
|
||||
return publicBaseUrl;
|
||||
}
|
||||
return uri.hasPort ? '${uri.host}:${uri.port}' : uri.host;
|
||||
}
|
||||
|
||||
static String? _normalizePublicBaseUrl(String? value) {
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return null;
|
||||
}
|
||||
final uri = Uri.tryParse(value.trim());
|
||||
if (uri == null || uri.host.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
if (uri.scheme != 'http' && uri.scheme != 'https') {
|
||||
return null;
|
||||
}
|
||||
return Uri(
|
||||
scheme: uri.scheme,
|
||||
host: uri.host,
|
||||
port: uri.hasPort ? uri.port : null,
|
||||
).toString().replaceAll(RegExp(r'/$'), '');
|
||||
}
|
||||
|
||||
static String? _legacyPublicBaseUrlFromHost(String? hostValue) {
|
||||
if (hostValue == null || hostValue.trim().isEmpty) {
|
||||
return null;
|
||||
}
|
||||
final host = hostValue.trim();
|
||||
if (host.contains('://')) {
|
||||
return host;
|
||||
}
|
||||
final lower = host.toLowerCase();
|
||||
final isLocal = lower.startsWith('localhost') || lower.startsWith('127.') || lower.startsWith('[::1]');
|
||||
final scheme = isLocal ? 'http' : 'https';
|
||||
return '$scheme://$host';
|
||||
}
|
||||
}
|
||||
+14
-132
@@ -1,22 +1,22 @@
|
||||
import 'package:objectbox/objectbox.dart';
|
||||
|
||||
/// User entity - works for both HOME and GUEST servers
|
||||
///
|
||||
///
|
||||
/// On HOME server:
|
||||
/// - isLocal = true
|
||||
/// - stores password, encrypted private key
|
||||
/// - this is where identity lives
|
||||
///
|
||||
///
|
||||
/// On GUEST server:
|
||||
/// - isLocal = false
|
||||
/// - homeServer contains the URL of user's home
|
||||
/// - publicKey cached for verification
|
||||
@Entity(uid: 3848231140411393897)
|
||||
class ChatUser {
|
||||
@Entity()
|
||||
class User {
|
||||
@Id()
|
||||
int id = 0;
|
||||
|
||||
// Global immutable identifier (opaque, host-independent).
|
||||
// Global identifier: "username@homeserver.com"
|
||||
@Unique()
|
||||
String userId;
|
||||
|
||||
@@ -28,7 +28,7 @@ class ChatUser {
|
||||
String? bio;
|
||||
|
||||
// Identity
|
||||
String publicKey; // ECDSA public key, always stored
|
||||
String publicKey; // Ed25519 public key, always stored
|
||||
|
||||
// HOME SERVER ONLY: Auth & key backup
|
||||
String? passwordHash;
|
||||
@@ -48,7 +48,7 @@ class ChatUser {
|
||||
@Property(type: PropertyType.date)
|
||||
DateTime? lastSyncedAt; // When profile was last refreshed from home
|
||||
|
||||
ChatUser({
|
||||
User({
|
||||
required this.userId,
|
||||
required this.username,
|
||||
required this.publicKey,
|
||||
@@ -68,8 +68,8 @@ class ChatUser {
|
||||
lastSyncedAt = lastSyncedAt;
|
||||
}
|
||||
|
||||
@Entity(uid: 2663861829934850300)
|
||||
class ChatChannel {
|
||||
@Entity()
|
||||
class Channel {
|
||||
@Id()
|
||||
int id = 0;
|
||||
|
||||
@@ -81,15 +81,15 @@ class ChatChannel {
|
||||
@Property(type: PropertyType.date)
|
||||
DateTime createdAt;
|
||||
|
||||
ChatChannel({
|
||||
Channel({
|
||||
required this.name,
|
||||
this.description,
|
||||
DateTime? createdAt,
|
||||
}) : createdAt = createdAt ?? DateTime.now();
|
||||
}
|
||||
|
||||
@Entity(uid: 2697867424319168785)
|
||||
class ChatMessage {
|
||||
@Entity()
|
||||
class Message {
|
||||
@Id()
|
||||
int id = 0;
|
||||
|
||||
@@ -97,7 +97,7 @@ class ChatMessage {
|
||||
|
||||
// Author info - includes enough for cross-server profile fetch
|
||||
int authorLocalId;
|
||||
String authorUserId; // Immutable userId
|
||||
String authorUserId; // "alice@homeserver.com"
|
||||
String? authorHomeServer; // for profile fetching
|
||||
|
||||
// Cached for display (snapshot at message time)
|
||||
@@ -105,7 +105,6 @@ class ChatMessage {
|
||||
String? authorAvatarUrl;
|
||||
|
||||
String content;
|
||||
String componentsJson;
|
||||
|
||||
@Property(type: PropertyType.date)
|
||||
DateTime createdAt;
|
||||
@@ -113,132 +112,15 @@ class ChatMessage {
|
||||
@Property(type: PropertyType.date)
|
||||
DateTime? editedAt;
|
||||
|
||||
ChatMessage({
|
||||
Message({
|
||||
required this.channelId,
|
||||
required this.authorLocalId,
|
||||
required this.authorUserId,
|
||||
required this.authorUsername,
|
||||
required this.content,
|
||||
required this.componentsJson,
|
||||
this.authorHomeServer,
|
||||
this.authorAvatarUrl,
|
||||
DateTime? createdAt,
|
||||
this.editedAt,
|
||||
}) : createdAt = createdAt ?? DateTime.now();
|
||||
}
|
||||
|
||||
@Entity()
|
||||
class ChatFile {
|
||||
@Id()
|
||||
int id = 0;
|
||||
|
||||
@Unique()
|
||||
String attachmentId;
|
||||
|
||||
int uploaderId;
|
||||
String path;
|
||||
String originalFileName;
|
||||
String storedFileName;
|
||||
String sha256;
|
||||
String mimeType;
|
||||
int width;
|
||||
int height;
|
||||
int sizeBytes;
|
||||
bool isAttached;
|
||||
String usageType; // message | avatar | server_icon | server_banner
|
||||
String status; // pending | ready | failed
|
||||
String? errorCode;
|
||||
|
||||
@Property(type: PropertyType.date)
|
||||
DateTime createdAt;
|
||||
|
||||
ChatFile({
|
||||
required this.attachmentId,
|
||||
required this.uploaderId,
|
||||
required this.path,
|
||||
required this.originalFileName,
|
||||
required this.storedFileName,
|
||||
required this.sha256,
|
||||
required this.mimeType,
|
||||
required this.width,
|
||||
required this.height,
|
||||
required this.sizeBytes,
|
||||
this.usageType = 'message',
|
||||
this.status = 'pending',
|
||||
this.isAttached = false,
|
||||
this.errorCode,
|
||||
DateTime? createdAt,
|
||||
}) : createdAt = createdAt ?? DateTime.now();
|
||||
}
|
||||
|
||||
@Entity()
|
||||
class ServerSettings {
|
||||
@Id()
|
||||
int id = 0;
|
||||
|
||||
@Unique()
|
||||
String singletonKey;
|
||||
|
||||
String serverName;
|
||||
String? description;
|
||||
String? iconAttachmentId;
|
||||
String? bannerAttachmentId;
|
||||
String? updatedByUserId;
|
||||
|
||||
@Property(type: PropertyType.date)
|
||||
DateTime updatedAt;
|
||||
|
||||
ServerSettings({
|
||||
this.singletonKey = 'default',
|
||||
required this.serverName,
|
||||
this.description,
|
||||
this.iconAttachmentId,
|
||||
this.bannerAttachmentId,
|
||||
this.updatedByUserId,
|
||||
DateTime? updatedAt,
|
||||
}) : updatedAt = updatedAt ?? DateTime.now();
|
||||
}
|
||||
|
||||
@Entity()
|
||||
class ChatMessageMedia {
|
||||
@Id()
|
||||
int id = 0;
|
||||
|
||||
int messageId;
|
||||
int channelId;
|
||||
String authorUserId;
|
||||
|
||||
// local_upload
|
||||
String? attachmentId;
|
||||
|
||||
// external_url
|
||||
String? externalUrl;
|
||||
|
||||
// image | video | file
|
||||
String kind;
|
||||
String? mimeType;
|
||||
String? originalFileName;
|
||||
int? sizeBytes;
|
||||
int? width;
|
||||
int? height;
|
||||
String? thumbHash;
|
||||
|
||||
@Property(type: PropertyType.date)
|
||||
DateTime createdAt;
|
||||
|
||||
ChatMessageMedia({
|
||||
required this.messageId,
|
||||
required this.channelId,
|
||||
required this.authorUserId,
|
||||
required this.kind,
|
||||
this.attachmentId,
|
||||
this.externalUrl,
|
||||
this.mimeType,
|
||||
this.originalFileName,
|
||||
this.sizeBytes,
|
||||
this.width,
|
||||
this.height,
|
||||
this.thumbHash,
|
||||
DateTime? createdAt,
|
||||
}) : createdAt = createdAt ?? DateTime.now();
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
{
|
||||
"_note1": "KEEP THIS FILE! Check it into a version control system (VCS) like git.",
|
||||
"_note2": "ObjectBox manages crucial IDs for your object model. See docs for details.",
|
||||
"_note3": "If you have VCS merge conflicts, you must resolve them according to ObjectBox docs.",
|
||||
"entities": [
|
||||
{
|
||||
"id": "4:8789103914473960036",
|
||||
"lastPropertyId": "4:1292940478834995143",
|
||||
"name": "ChatChannel",
|
||||
"properties": [
|
||||
{
|
||||
"id": "1:8326949064500635093",
|
||||
"name": "id",
|
||||
"type": 6,
|
||||
"flags": 1
|
||||
},
|
||||
{
|
||||
"id": "2:4261805573466490477",
|
||||
"name": "name",
|
||||
"indexId": "5:7489629953476729908",
|
||||
"type": 9,
|
||||
"flags": 2080
|
||||
},
|
||||
{
|
||||
"id": "3:3159087232030070914",
|
||||
"name": "description",
|
||||
"type": 9
|
||||
},
|
||||
{
|
||||
"id": "4:1292940478834995143",
|
||||
"name": "createdAt",
|
||||
"type": 10
|
||||
}
|
||||
],
|
||||
"relations": []
|
||||
},
|
||||
{
|
||||
"id": "5:3737221321740284134",
|
||||
"lastPropertyId": "5:9202752608552942529",
|
||||
"name": "ChatMessage",
|
||||
"properties": [
|
||||
{
|
||||
"id": "1:6937877991903739354",
|
||||
"name": "id",
|
||||
"type": 6,
|
||||
"flags": 1
|
||||
},
|
||||
{
|
||||
"id": "2:2320935452587221036",
|
||||
"name": "content",
|
||||
"type": 9
|
||||
},
|
||||
{
|
||||
"id": "3:4252783758825304742",
|
||||
"name": "senderId",
|
||||
"indexId": "6:1080757755541990100",
|
||||
"type": 6,
|
||||
"flags": 8
|
||||
},
|
||||
{
|
||||
"id": "4:3240604049002701987",
|
||||
"name": "channelId",
|
||||
"indexId": "7:2720778762517392200",
|
||||
"type": 6,
|
||||
"flags": 8
|
||||
},
|
||||
{
|
||||
"id": "5:9202752608552942529",
|
||||
"name": "createdAt",
|
||||
"type": 10
|
||||
}
|
||||
],
|
||||
"relations": []
|
||||
},
|
||||
{
|
||||
"id": "6:6808595317639142314",
|
||||
"lastPropertyId": "4:3756250635467596289",
|
||||
"name": "ChatUser",
|
||||
"properties": [
|
||||
{
|
||||
"id": "1:5564900411312083069",
|
||||
"name": "id",
|
||||
"type": 6,
|
||||
"flags": 1
|
||||
},
|
||||
{
|
||||
"id": "2:3278062150453672411",
|
||||
"name": "username",
|
||||
"indexId": "8:3068871702763915304",
|
||||
"type": 9,
|
||||
"flags": 2048
|
||||
},
|
||||
{
|
||||
"id": "3:7002102783671264440",
|
||||
"name": "passwordHash",
|
||||
"type": 9
|
||||
},
|
||||
{
|
||||
"id": "4:3756250635467596289",
|
||||
"name": "createdAt",
|
||||
"type": 10
|
||||
}
|
||||
],
|
||||
"relations": []
|
||||
}
|
||||
],
|
||||
"lastEntityId": "6:6808595317639142314",
|
||||
"lastIndexId": "8:3068871702763915304",
|
||||
"lastRelationId": "0:0",
|
||||
"lastSequenceId": "0:0",
|
||||
"modelVersion": 5,
|
||||
"modelVersionParserMinimum": 5,
|
||||
"retiredEntityUids": [
|
||||
240536022189472816,
|
||||
8077011728318081304,
|
||||
4705468874025958717
|
||||
],
|
||||
"retiredIndexUids": [],
|
||||
"retiredPropertyUids": [
|
||||
8544617182402792569,
|
||||
6117431287454466914,
|
||||
5437786674528658195,
|
||||
6072852454810870218,
|
||||
2542181165384894966,
|
||||
864989412971989074,
|
||||
8028710060941408150,
|
||||
1587682278007620331,
|
||||
4383284664306084614,
|
||||
8714147817359246506,
|
||||
7393508199779059949,
|
||||
3791306853268708842,
|
||||
6999720138118727677
|
||||
],
|
||||
"retiredRelationUids": [],
|
||||
"version": 1
|
||||
}
|
||||
@@ -0,0 +1,418 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// This code was generated by ObjectBox. To update it run the generator again
|
||||
// with `dart run build_runner build`.
|
||||
// See also https://docs.objectbox.io/getting-started#generate-objectbox-code
|
||||
|
||||
// ignore_for_file: camel_case_types, depend_on_referenced_packages
|
||||
// coverage:ignore-file
|
||||
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flat_buffers/flat_buffers.dart' as fb;
|
||||
import 'package:objectbox/internal.dart'
|
||||
as obx_int; // generated code can access "internal" functionality
|
||||
import 'package:objectbox/objectbox.dart' as obx;
|
||||
|
||||
import 'models.dart';
|
||||
|
||||
export 'package:objectbox/objectbox.dart'; // so that callers only have to import this file
|
||||
|
||||
final _entities = <obx_int.ModelEntity>[
|
||||
obx_int.ModelEntity(
|
||||
id: const obx_int.IdUid(4, 8789103914473960036),
|
||||
name: 'ChatChannel',
|
||||
lastPropertyId: const obx_int.IdUid(4, 1292940478834995143),
|
||||
flags: 0,
|
||||
properties: <obx_int.ModelProperty>[
|
||||
obx_int.ModelProperty(
|
||||
id: const obx_int.IdUid(1, 8326949064500635093),
|
||||
name: 'id',
|
||||
type: 6,
|
||||
flags: 1,
|
||||
),
|
||||
obx_int.ModelProperty(
|
||||
id: const obx_int.IdUid(2, 4261805573466490477),
|
||||
name: 'name',
|
||||
type: 9,
|
||||
flags: 2080,
|
||||
indexId: const obx_int.IdUid(5, 7489629953476729908),
|
||||
),
|
||||
obx_int.ModelProperty(
|
||||
id: const obx_int.IdUid(3, 3159087232030070914),
|
||||
name: 'description',
|
||||
type: 9,
|
||||
flags: 0,
|
||||
),
|
||||
obx_int.ModelProperty(
|
||||
id: const obx_int.IdUid(4, 1292940478834995143),
|
||||
name: 'createdAt',
|
||||
type: 10,
|
||||
flags: 0,
|
||||
),
|
||||
],
|
||||
relations: <obx_int.ModelRelation>[],
|
||||
backlinks: <obx_int.ModelBacklink>[],
|
||||
),
|
||||
obx_int.ModelEntity(
|
||||
id: const obx_int.IdUid(5, 3737221321740284134),
|
||||
name: 'ChatMessage',
|
||||
lastPropertyId: const obx_int.IdUid(5, 9202752608552942529),
|
||||
flags: 0,
|
||||
properties: <obx_int.ModelProperty>[
|
||||
obx_int.ModelProperty(
|
||||
id: const obx_int.IdUid(1, 6937877991903739354),
|
||||
name: 'id',
|
||||
type: 6,
|
||||
flags: 1,
|
||||
),
|
||||
obx_int.ModelProperty(
|
||||
id: const obx_int.IdUid(2, 2320935452587221036),
|
||||
name: 'content',
|
||||
type: 9,
|
||||
flags: 0,
|
||||
),
|
||||
obx_int.ModelProperty(
|
||||
id: const obx_int.IdUid(3, 4252783758825304742),
|
||||
name: 'senderId',
|
||||
type: 6,
|
||||
flags: 8,
|
||||
indexId: const obx_int.IdUid(6, 1080757755541990100),
|
||||
),
|
||||
obx_int.ModelProperty(
|
||||
id: const obx_int.IdUid(4, 3240604049002701987),
|
||||
name: 'channelId',
|
||||
type: 6,
|
||||
flags: 8,
|
||||
indexId: const obx_int.IdUid(7, 2720778762517392200),
|
||||
),
|
||||
obx_int.ModelProperty(
|
||||
id: const obx_int.IdUid(5, 9202752608552942529),
|
||||
name: 'createdAt',
|
||||
type: 10,
|
||||
flags: 0,
|
||||
),
|
||||
],
|
||||
relations: <obx_int.ModelRelation>[],
|
||||
backlinks: <obx_int.ModelBacklink>[],
|
||||
),
|
||||
obx_int.ModelEntity(
|
||||
id: const obx_int.IdUid(6, 6808595317639142314),
|
||||
name: 'ChatUser',
|
||||
lastPropertyId: const obx_int.IdUid(4, 3756250635467596289),
|
||||
flags: 0,
|
||||
properties: <obx_int.ModelProperty>[
|
||||
obx_int.ModelProperty(
|
||||
id: const obx_int.IdUid(1, 5564900411312083069),
|
||||
name: 'id',
|
||||
type: 6,
|
||||
flags: 1,
|
||||
),
|
||||
obx_int.ModelProperty(
|
||||
id: const obx_int.IdUid(2, 3278062150453672411),
|
||||
name: 'username',
|
||||
type: 9,
|
||||
flags: 2048,
|
||||
indexId: const obx_int.IdUid(8, 3068871702763915304),
|
||||
),
|
||||
obx_int.ModelProperty(
|
||||
id: const obx_int.IdUid(3, 7002102783671264440),
|
||||
name: 'passwordHash',
|
||||
type: 9,
|
||||
flags: 0,
|
||||
),
|
||||
obx_int.ModelProperty(
|
||||
id: const obx_int.IdUid(4, 3756250635467596289),
|
||||
name: 'createdAt',
|
||||
type: 10,
|
||||
flags: 0,
|
||||
),
|
||||
],
|
||||
relations: <obx_int.ModelRelation>[],
|
||||
backlinks: <obx_int.ModelBacklink>[],
|
||||
),
|
||||
];
|
||||
|
||||
/// Shortcut for [obx.Store.new] that passes [getObjectBoxModel] and for Flutter
|
||||
/// apps by default a [directory] using `defaultStoreDirectory()` from the
|
||||
/// ObjectBox Flutter library.
|
||||
///
|
||||
/// Note: for desktop apps it is recommended to specify a unique [directory].
|
||||
///
|
||||
/// See [obx.Store.new] for an explanation of all parameters.
|
||||
///
|
||||
/// For Flutter apps, also calls `loadObjectBoxLibraryAndroidCompat()` from
|
||||
/// the ObjectBox Flutter library to fix loading the native ObjectBox library
|
||||
/// on Android 6 and older.
|
||||
obx.Store openStore({
|
||||
String? directory,
|
||||
int? maxDBSizeInKB,
|
||||
int? maxDataSizeInKB,
|
||||
int? fileMode,
|
||||
int? maxReaders,
|
||||
bool queriesCaseSensitiveDefault = true,
|
||||
String? macosApplicationGroup,
|
||||
}) {
|
||||
return obx.Store(
|
||||
getObjectBoxModel(),
|
||||
directory: directory,
|
||||
maxDBSizeInKB: maxDBSizeInKB,
|
||||
maxDataSizeInKB: maxDataSizeInKB,
|
||||
fileMode: fileMode,
|
||||
maxReaders: maxReaders,
|
||||
queriesCaseSensitiveDefault: queriesCaseSensitiveDefault,
|
||||
macosApplicationGroup: macosApplicationGroup,
|
||||
);
|
||||
}
|
||||
|
||||
/// Returns the ObjectBox model definition for this project for use with
|
||||
/// [obx.Store.new].
|
||||
obx_int.ModelDefinition getObjectBoxModel() {
|
||||
final model = obx_int.ModelInfo(
|
||||
// If this version is not found, it means that this file was generated
|
||||
// with an older version of the ObjectBox Dart generator.
|
||||
// Please regenerate this file with the current generator version.
|
||||
// Typically, this is done with `dart run build_runner build`.
|
||||
generatorVersion: obx_int.GeneratorVersion.v2025_12_16,
|
||||
entities: _entities,
|
||||
lastEntityId: const obx_int.IdUid(6, 6808595317639142314),
|
||||
lastIndexId: const obx_int.IdUid(8, 3068871702763915304),
|
||||
lastRelationId: const obx_int.IdUid(0, 0),
|
||||
lastSequenceId: const obx_int.IdUid(0, 0),
|
||||
retiredEntityUids: const [
|
||||
240536022189472816,
|
||||
8077011728318081304,
|
||||
4705468874025958717,
|
||||
],
|
||||
retiredIndexUids: const [],
|
||||
retiredPropertyUids: const [
|
||||
8544617182402792569,
|
||||
6117431287454466914,
|
||||
5437786674528658195,
|
||||
6072852454810870218,
|
||||
2542181165384894966,
|
||||
864989412971989074,
|
||||
8028710060941408150,
|
||||
1587682278007620331,
|
||||
4383284664306084614,
|
||||
8714147817359246506,
|
||||
7393508199779059949,
|
||||
3791306853268708842,
|
||||
6999720138118727677,
|
||||
],
|
||||
retiredRelationUids: const [],
|
||||
modelVersion: 5,
|
||||
modelVersionParserMinimum: 5,
|
||||
version: 1,
|
||||
);
|
||||
|
||||
final bindings = <Type, obx_int.EntityDefinition>{
|
||||
ChatChannel: obx_int.EntityDefinition<ChatChannel>(
|
||||
model: _entities[0],
|
||||
toOneRelations: (ChatChannel object) => [],
|
||||
toManyRelations: (ChatChannel object) => {},
|
||||
getId: (ChatChannel object) => object.id,
|
||||
setId: (ChatChannel object, int id) {
|
||||
object.id = id;
|
||||
},
|
||||
objectToFB: (ChatChannel object, fb.Builder fbb) {
|
||||
final nameOffset = fbb.writeString(object.name);
|
||||
final descriptionOffset = object.description == null
|
||||
? null
|
||||
: fbb.writeString(object.description!);
|
||||
fbb.startTable(5);
|
||||
fbb.addInt64(0, object.id);
|
||||
fbb.addOffset(1, nameOffset);
|
||||
fbb.addOffset(2, descriptionOffset);
|
||||
fbb.addInt64(3, object.createdAt.millisecondsSinceEpoch);
|
||||
fbb.finish(fbb.endTable());
|
||||
return object.id;
|
||||
},
|
||||
objectFromFB: (obx.Store store, ByteData fbData) {
|
||||
final buffer = fb.BufferContext(fbData);
|
||||
final rootOffset = buffer.derefObject(0);
|
||||
final nameParam = const fb.StringReader(
|
||||
asciiOptimization: true,
|
||||
).vTableGet(buffer, rootOffset, 6, '');
|
||||
final descriptionParam = const fb.StringReader(
|
||||
asciiOptimization: true,
|
||||
).vTableGetNullable(buffer, rootOffset, 8);
|
||||
final createdAtParam = DateTime.fromMillisecondsSinceEpoch(
|
||||
const fb.Int64Reader().vTableGet(buffer, rootOffset, 10, 0),
|
||||
);
|
||||
final object = ChatChannel(
|
||||
name: nameParam,
|
||||
description: descriptionParam,
|
||||
createdAt: createdAtParam,
|
||||
)..id = const fb.Int64Reader().vTableGet(buffer, rootOffset, 4, 0);
|
||||
|
||||
return object;
|
||||
},
|
||||
),
|
||||
ChatMessage: obx_int.EntityDefinition<ChatMessage>(
|
||||
model: _entities[1],
|
||||
toOneRelations: (ChatMessage object) => [],
|
||||
toManyRelations: (ChatMessage object) => {},
|
||||
getId: (ChatMessage object) => object.id,
|
||||
setId: (ChatMessage object, int id) {
|
||||
object.id = id;
|
||||
},
|
||||
objectToFB: (ChatMessage object, fb.Builder fbb) {
|
||||
final contentOffset = fbb.writeString(object.content);
|
||||
fbb.startTable(6);
|
||||
fbb.addInt64(0, object.id);
|
||||
fbb.addOffset(1, contentOffset);
|
||||
fbb.addInt64(2, object.senderId);
|
||||
fbb.addInt64(3, object.channelId);
|
||||
fbb.addInt64(4, object.createdAt.millisecondsSinceEpoch);
|
||||
fbb.finish(fbb.endTable());
|
||||
return object.id;
|
||||
},
|
||||
objectFromFB: (obx.Store store, ByteData fbData) {
|
||||
final buffer = fb.BufferContext(fbData);
|
||||
final rootOffset = buffer.derefObject(0);
|
||||
final contentParam = const fb.StringReader(
|
||||
asciiOptimization: true,
|
||||
).vTableGet(buffer, rootOffset, 6, '');
|
||||
final senderIdParam = const fb.Int64Reader().vTableGet(
|
||||
buffer,
|
||||
rootOffset,
|
||||
8,
|
||||
0,
|
||||
);
|
||||
final channelIdParam = const fb.Int64Reader().vTableGet(
|
||||
buffer,
|
||||
rootOffset,
|
||||
10,
|
||||
0,
|
||||
);
|
||||
final createdAtParam = DateTime.fromMillisecondsSinceEpoch(
|
||||
const fb.Int64Reader().vTableGet(buffer, rootOffset, 12, 0),
|
||||
);
|
||||
final object = ChatMessage(
|
||||
content: contentParam,
|
||||
senderId: senderIdParam,
|
||||
channelId: channelIdParam,
|
||||
createdAt: createdAtParam,
|
||||
)..id = const fb.Int64Reader().vTableGet(buffer, rootOffset, 4, 0);
|
||||
|
||||
return object;
|
||||
},
|
||||
),
|
||||
ChatUser: obx_int.EntityDefinition<ChatUser>(
|
||||
model: _entities[2],
|
||||
toOneRelations: (ChatUser object) => [],
|
||||
toManyRelations: (ChatUser object) => {},
|
||||
getId: (ChatUser object) => object.id,
|
||||
setId: (ChatUser object, int id) {
|
||||
object.id = id;
|
||||
},
|
||||
objectToFB: (ChatUser object, fb.Builder fbb) {
|
||||
final usernameOffset = fbb.writeString(object.username);
|
||||
final passwordHashOffset = fbb.writeString(object.passwordHash);
|
||||
fbb.startTable(5);
|
||||
fbb.addInt64(0, object.id);
|
||||
fbb.addOffset(1, usernameOffset);
|
||||
fbb.addOffset(2, passwordHashOffset);
|
||||
fbb.addInt64(3, object.createdAt.millisecondsSinceEpoch);
|
||||
fbb.finish(fbb.endTable());
|
||||
return object.id;
|
||||
},
|
||||
objectFromFB: (obx.Store store, ByteData fbData) {
|
||||
final buffer = fb.BufferContext(fbData);
|
||||
final rootOffset = buffer.derefObject(0);
|
||||
final usernameParam = const fb.StringReader(
|
||||
asciiOptimization: true,
|
||||
).vTableGet(buffer, rootOffset, 6, '');
|
||||
final passwordHashParam = const fb.StringReader(
|
||||
asciiOptimization: true,
|
||||
).vTableGet(buffer, rootOffset, 8, '');
|
||||
final createdAtParam = DateTime.fromMillisecondsSinceEpoch(
|
||||
const fb.Int64Reader().vTableGet(buffer, rootOffset, 10, 0),
|
||||
);
|
||||
final object = ChatUser(
|
||||
username: usernameParam,
|
||||
passwordHash: passwordHashParam,
|
||||
createdAt: createdAtParam,
|
||||
)..id = const fb.Int64Reader().vTableGet(buffer, rootOffset, 4, 0);
|
||||
|
||||
return object;
|
||||
},
|
||||
),
|
||||
};
|
||||
|
||||
return obx_int.ModelDefinition(model, bindings);
|
||||
}
|
||||
|
||||
/// [ChatChannel] entity fields to define ObjectBox queries.
|
||||
class ChatChannel_ {
|
||||
/// See [ChatChannel.id].
|
||||
static final id = obx.QueryIntegerProperty<ChatChannel>(
|
||||
_entities[0].properties[0],
|
||||
);
|
||||
|
||||
/// See [ChatChannel.name].
|
||||
static final name = obx.QueryStringProperty<ChatChannel>(
|
||||
_entities[0].properties[1],
|
||||
);
|
||||
|
||||
/// See [ChatChannel.description].
|
||||
static final description = obx.QueryStringProperty<ChatChannel>(
|
||||
_entities[0].properties[2],
|
||||
);
|
||||
|
||||
/// See [ChatChannel.createdAt].
|
||||
static final createdAt = obx.QueryDateProperty<ChatChannel>(
|
||||
_entities[0].properties[3],
|
||||
);
|
||||
}
|
||||
|
||||
/// [ChatMessage] entity fields to define ObjectBox queries.
|
||||
class ChatMessage_ {
|
||||
/// See [ChatMessage.id].
|
||||
static final id = obx.QueryIntegerProperty<ChatMessage>(
|
||||
_entities[1].properties[0],
|
||||
);
|
||||
|
||||
/// See [ChatMessage.content].
|
||||
static final content = obx.QueryStringProperty<ChatMessage>(
|
||||
_entities[1].properties[1],
|
||||
);
|
||||
|
||||
/// See [ChatMessage.senderId].
|
||||
static final senderId = obx.QueryIntegerProperty<ChatMessage>(
|
||||
_entities[1].properties[2],
|
||||
);
|
||||
|
||||
/// See [ChatMessage.channelId].
|
||||
static final channelId = obx.QueryIntegerProperty<ChatMessage>(
|
||||
_entities[1].properties[3],
|
||||
);
|
||||
|
||||
/// See [ChatMessage.createdAt].
|
||||
static final createdAt = obx.QueryDateProperty<ChatMessage>(
|
||||
_entities[1].properties[4],
|
||||
);
|
||||
}
|
||||
|
||||
/// [ChatUser] entity fields to define ObjectBox queries.
|
||||
class ChatUser_ {
|
||||
/// See [ChatUser.id].
|
||||
static final id = obx.QueryIntegerProperty<ChatUser>(
|
||||
_entities[2].properties[0],
|
||||
);
|
||||
|
||||
/// See [ChatUser.username].
|
||||
static final username = obx.QueryStringProperty<ChatUser>(
|
||||
_entities[2].properties[1],
|
||||
);
|
||||
|
||||
/// See [ChatUser.passwordHash].
|
||||
static final passwordHash = obx.QueryStringProperty<ChatUser>(
|
||||
_entities[2].properties[2],
|
||||
);
|
||||
|
||||
/// See [ChatUser.createdAt].
|
||||
static final createdAt = obx.QueryDateProperty<ChatUser>(
|
||||
_entities[2].properties[3],
|
||||
);
|
||||
}
|
||||
+40
-59
@@ -1,18 +1,21 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:chat_server/models.dart';
|
||||
import 'package:chat_server/objectbox.g.dart';
|
||||
import 'package:hotreloader/hotreloader.dart';
|
||||
import 'package:relic/relic.dart';
|
||||
|
||||
import 'config/config.dart';
|
||||
import 'src/auth/auth_middleware.dart';
|
||||
import 'src/gateway/gateway_handlers.dart';
|
||||
import 'src/handlers/auth_handlers.dart';
|
||||
import 'src/handlers/channel_handlers.dart';
|
||||
import 'src/handlers/dev_db_handlers.dart';
|
||||
import 'src/handlers/message_handlers.dart';
|
||||
import 'src/handlers/public_handlers.dart';
|
||||
import 'src/handlers/settings_handlers.dart';
|
||||
import 'src/handlers/upload_handlers.dart';
|
||||
import 'src/state/app_state.dart';
|
||||
import 'src/utils/extensions.dart';
|
||||
|
||||
// Global ObjectBox store and boxes
|
||||
late final Store store;
|
||||
late final Box<User> userBox;
|
||||
late final Box<Channel> channelBox;
|
||||
late final Box<Message> messageBox;
|
||||
|
||||
RelicApp? _currentApp;
|
||||
|
||||
@@ -21,85 +24,63 @@ Future<void> startServer() async {
|
||||
|
||||
_currentApp = RelicApp()
|
||||
// ============ PUBLIC ENDPOINTS (no auth) ============
|
||||
|
||||
|
||||
// Auth
|
||||
..post('/auth/register', registerHandler)
|
||||
..post('/auth/login', loginHandler)
|
||||
..post('/auth/join', joinServerHandler)
|
||||
|
||||
|
||||
// Public identity verification (for cross-server)
|
||||
..get('/.well-known/users/:username', publicUserHandler)
|
||||
..get('/.well-known/verify/:userId', publicVerifyHandler)
|
||||
..get('/.well-known/users/<username>', publicUserHandler)
|
||||
..get('/.well-known/verify/<userId>', publicVerifyHandler)
|
||||
..get('/.well-known/server', publicServerInfoHandler)
|
||||
..get('/uploads/:fileName', getUploadFileHandler)
|
||||
..get('/attachments/:attachmentId', getAttachmentHandler)
|
||||
..get('/avatars/:userId', getAvatarAliasHandler)
|
||||
..get('/icons/:serverId', getServerIconAliasHandler)
|
||||
..get('/banners/:serverId', getServerBannerAliasHandler)
|
||||
..get('/gateway', gatewayHandler)
|
||||
|
||||
|
||||
// ============ PROTECTED ENDPOINTS (requires auth) ============
|
||||
..use('/api', jwtMiddleware)
|
||||
|
||||
|
||||
// Current user
|
||||
..get('/api/me', getMeHandler)
|
||||
..patch('/api/me', updateProfileHandler)
|
||||
..put('/api/me/avatar', setAvatarHandler)
|
||||
|
||||
// Server settings
|
||||
..get('/api/server/settings', getServerSettingsHandler)
|
||||
..patch('/api/server/settings', patchServerSettingsHandler)
|
||||
..put('/api/server/icon', setServerIconHandler)
|
||||
..put('/api/server/banner', setServerBannerHandler)
|
||||
|
||||
// Dev DB tools (testing only, admin only, requires config flag)
|
||||
..get('/api/dev/db/snapshot', getDevDbSnapshotHandler)
|
||||
..post('/api/dev/db/reset', resetDevDbHandler)
|
||||
..post('/api/dev/users/promote-admin', promoteUserToAdminHandler)
|
||||
..post('/api/dev/users/demote-admin', demoteUserFromAdminHandler)
|
||||
|
||||
|
||||
// Channels
|
||||
..get('/api/channels', getChannelsHandler)
|
||||
..post('/api/channels', createChannelHandler)
|
||||
..get('/api/channels/:channelId', getChannelHandler)
|
||||
|
||||
..get('/api/channels/<channelId>', getChannelHandler)
|
||||
|
||||
// Messages
|
||||
..get('/api/messages', getMessagesHandler)
|
||||
..get('/api/channels/:channelId/media', getChannelMediaHandler)
|
||||
..post('/api/uploads/init', initUploadHandler)
|
||||
..put('/api/uploads/:uploadId', completeUploadHandler)
|
||||
..post('/api/uploads', uploadFileHandler)
|
||||
..post('/api/messages', sendMessageHandler)
|
||||
..put('/api/messages/:messageId', editMessageHandler)
|
||||
..delete('/api/messages/:messageId', deleteMessageHandler)
|
||||
|
||||
..put('/api/messages/<messageId>', editMessageHandler)
|
||||
..delete('/api/messages/<messageId>', deleteMessageHandler)
|
||||
|
||||
// ============ LOGGING & FALLBACK ============
|
||||
..use('/', logRequests())
|
||||
..fallback = respondWith((_) => Response.notFound());
|
||||
|
||||
print('');
|
||||
print('🚀 Server starting...');
|
||||
print('Bind: ${'${Config.bindHost}:${Config.bindPort}'.padRight(34)}');
|
||||
print('Public URL: ${Config.publicBaseUrl.padRight(26)}');
|
||||
print('╔════════════════════════════════════════════╗');
|
||||
print('║ 🚀 Server starting... ║');
|
||||
print('║ Host: ${Config.serverHost.padRight(34)}║');
|
||||
print('║ URL: http://localhost:${Config.port.toString().padRight(26)}║');
|
||||
print('╚════════════════════════════════════════════╝');
|
||||
print('');
|
||||
|
||||
final bindAddress = InternetAddress.tryParse(Config.bindHost) ?? (await InternetAddress.lookup(Config.bindHost)).first;
|
||||
|
||||
await _currentApp!.serve(
|
||||
address: bindAddress,
|
||||
port: Config.bindPort,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> stopServer() async {
|
||||
await _currentApp?.close();
|
||||
_currentApp = null;
|
||||
await _currentApp!.serve(port: Config.port);
|
||||
}
|
||||
|
||||
void main() async {
|
||||
Config.initialize();
|
||||
// Initialize ObjectBox
|
||||
store = openStore();
|
||||
userBox = store.box<User>();
|
||||
channelBox = store.box<Channel>();
|
||||
messageBox = store.box<Message>();
|
||||
|
||||
initializeStore();
|
||||
// Enable hot reload in development
|
||||
await HotReloader.create(
|
||||
onAfterReload: (ctx) async {
|
||||
print('🔄 Changes detected! Restarting server...');
|
||||
await startServer();
|
||||
},
|
||||
);
|
||||
|
||||
await startServer();
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import 'package:relic/relic.dart';
|
||||
|
||||
import '../../config/config.dart';
|
||||
import '../utils/extensions.dart';
|
||||
import 'crypto_service.dart';
|
||||
|
||||
final userIdProperty = ContextProperty<String>('userId');
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import 'package:bcrypt/bcrypt.dart';
|
||||
import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart';
|
||||
|
||||
import '../../config/config.dart';
|
||||
|
||||
class AuthService {
|
||||
/// Generates a JWT token for a user
|
||||
static String generateToken(String userId, {String? username}) {
|
||||
final payload = {'sub': userId, 'username': ?username};
|
||||
|
||||
final jwt = JWT(payload);
|
||||
return jwt.sign(SecretKey(Config.jwtSecret), expiresIn: Config.tokenExpiry);
|
||||
}
|
||||
|
||||
/// Verifies a password against a bcrypt hash
|
||||
static bool verifyPassword(String password, String hash) {
|
||||
return BCrypt.checkpw(password, hash);
|
||||
}
|
||||
|
||||
/// Hashes a password using bcrypt
|
||||
static String hashPassword(String password) {
|
||||
return BCrypt.hashpw(password, BCrypt.gensalt());
|
||||
}
|
||||
|
||||
/// Verifies a JWT token and returns the payload
|
||||
static Map<String, dynamic>? verifyToken(String token) {
|
||||
try {
|
||||
final jwt = JWT.verify(token, SecretKey(Config.jwtSecret));
|
||||
return Map<String, dynamic>.from(jwt.payload as Map);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:bcrypt/bcrypt.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:crypton/crypton.dart' as crypton;
|
||||
import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart' as jwt;
|
||||
import 'package:crypton/crypton.dart';
|
||||
import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart';
|
||||
|
||||
import '../../config/config.dart';
|
||||
import '../config/config.dart';
|
||||
|
||||
class KeyPair {
|
||||
final String publicKey;
|
||||
@@ -29,9 +29,9 @@ class KeyPair {
|
||||
}
|
||||
|
||||
class CryptoService {
|
||||
/// Generate a new ECDSA key pair
|
||||
/// Generate a new Ed25519 key pair
|
||||
static KeyPair generateKeyPair() {
|
||||
final keyPair = crypton.ECKeypair.fromRandom();
|
||||
final keyPair = ECKeypair.fromRandom();
|
||||
return KeyPair(
|
||||
publicKey: keyPair.publicKey.toString(),
|
||||
privateKey: keyPair.privateKey.toString(),
|
||||
@@ -40,19 +40,17 @@ class CryptoService {
|
||||
|
||||
/// Sign data with private key
|
||||
static String sign(String data, String privateKey) {
|
||||
final privKey = crypton.ECPrivateKey.fromString(privateKey);
|
||||
final message = Uint8List.fromList(utf8.encode(data));
|
||||
final signature = privKey.createSHA256Signature(message);
|
||||
return base64Encode(signature);
|
||||
final privKey = ECPrivateKey.fromString(privateKey);
|
||||
final signature = privKey.createSignature(data);
|
||||
return signature.toString();
|
||||
}
|
||||
|
||||
/// Verify signature with public key
|
||||
static bool verify(String data, String signature, String publicKey) {
|
||||
try {
|
||||
final pubKey = crypton.ECPublicKey.fromString(publicKey);
|
||||
final message = Uint8List.fromList(utf8.encode(data));
|
||||
final signatureBytes = Uint8List.fromList(base64Decode(signature));
|
||||
return pubKey.verifySHA256Signature(message, signatureBytes);
|
||||
final pubKey = ECPublicKey.fromString(publicKey);
|
||||
final sig = ECSignature.fromString(signature);
|
||||
return pubKey.verifySignature(data, sig);
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
@@ -120,8 +118,6 @@ class EncryptedKey {
|
||||
}
|
||||
|
||||
class AuthService {
|
||||
static final _nonceRandom = Random.secure();
|
||||
|
||||
/// Generate JWT token for user
|
||||
static String generateToken(String userId, String username) {
|
||||
final payload = {
|
||||
@@ -129,9 +125,9 @@ class AuthService {
|
||||
'username': username,
|
||||
};
|
||||
|
||||
final token = jwt.JWT(payload);
|
||||
return token.sign(
|
||||
jwt.SecretKey(Config.jwtSecret),
|
||||
final jwt = JWT(payload);
|
||||
return jwt.sign(
|
||||
SecretKey(Config.jwtSecret),
|
||||
expiresIn: Config.tokenExpiry,
|
||||
);
|
||||
}
|
||||
@@ -139,8 +135,8 @@ class AuthService {
|
||||
/// Verify JWT token and return payload
|
||||
static Map<String, dynamic>? verifyToken(String token) {
|
||||
try {
|
||||
final verified = jwt.JWT.verify(token, jwt.SecretKey(Config.jwtSecret));
|
||||
return Map<String, dynamic>.from(verified.payload as Map);
|
||||
final jwt = JWT.verify(token, SecretKey(Config.jwtSecret));
|
||||
return Map<String, dynamic>.from(jwt.payload as Map);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
@@ -158,8 +154,8 @@ class AuthService {
|
||||
|
||||
/// Generate a nonce for signature verification
|
||||
static String generateNonce() {
|
||||
final bytes = List<int>.generate(32, (_) => _nonceRandom.nextInt(256));
|
||||
final hash = sha256.convert(bytes);
|
||||
return hash.toString().substring(0, 32);
|
||||
final bytes = List<int>.generate(32, (_) => DateTime.now().microsecondsSinceEpoch % 256);
|
||||
final randomBytes = sha256.convert(bytes as Uint8List);
|
||||
return randomBytes.toString().substring(0, 32);
|
||||
}
|
||||
}
|
||||
@@ -1,442 +0,0 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:relic/relic.dart';
|
||||
import 'package:web_socket/web_socket.dart';
|
||||
|
||||
import '../auth/crypto_service.dart';
|
||||
import '../state/app_state.dart';
|
||||
|
||||
final _gatewayRegistry = _GatewayRegistry();
|
||||
|
||||
/// WebSocket gateway with JWT auth and channel subscriptions.
|
||||
Result gatewayHandler(Request req) {
|
||||
final token = _extractGatewayToken(req);
|
||||
if (token == null) {
|
||||
return Response.unauthorized(
|
||||
body: Body.fromString('Bearer token required'),
|
||||
);
|
||||
}
|
||||
|
||||
final payload = AuthService.verifyToken(token);
|
||||
if (payload == null) {
|
||||
return Response.forbidden(
|
||||
body: Body.fromString('Invalid or expired token'),
|
||||
);
|
||||
}
|
||||
|
||||
final userId = payload['sub'] as String?;
|
||||
final username = payload['username'] as String?;
|
||||
if (userId == null || userId.isEmpty) {
|
||||
return Response.forbidden(
|
||||
body: Body.fromString('Invalid token payload'),
|
||||
);
|
||||
}
|
||||
|
||||
return WebSocketUpgrade((webSocket) {
|
||||
webSocket.pingInterval = const Duration(seconds: 30);
|
||||
|
||||
final connection = _gatewayRegistry.register(
|
||||
socket: webSocket,
|
||||
userId: userId,
|
||||
username: username,
|
||||
);
|
||||
|
||||
_sendGatewayPayload(
|
||||
connection.id,
|
||||
{
|
||||
'op': 'ready',
|
||||
'connectionId': connection.id,
|
||||
'userId': connection.userId,
|
||||
if (connection.username != null) 'username': connection.username,
|
||||
},
|
||||
);
|
||||
|
||||
webSocket.events.listen(
|
||||
(event) {
|
||||
switch (event) {
|
||||
case TextDataReceived(text: final text):
|
||||
_handleGatewayTextMessage(connection.id, text);
|
||||
case BinaryDataReceived():
|
||||
_sendProtocolError(
|
||||
connection.id,
|
||||
'Binary payloads are not supported',
|
||||
);
|
||||
case CloseReceived():
|
||||
_gatewayRegistry.unregister(connection.id);
|
||||
}
|
||||
},
|
||||
onError: (_, __) {
|
||||
_gatewayRegistry.unregister(connection.id);
|
||||
},
|
||||
onDone: () {
|
||||
_gatewayRegistry.unregister(connection.id);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void broadcastMessageCreated({
|
||||
required int channelId,
|
||||
required Map<String, dynamic> message,
|
||||
}) {
|
||||
_gatewayRegistry.broadcastToChannel(channelId, {
|
||||
'op': 'event',
|
||||
'event': 'message.created',
|
||||
'channelId': channelId,
|
||||
'message': message,
|
||||
'emittedAt': DateTime.now().toUtc().toIso8601String(),
|
||||
});
|
||||
}
|
||||
|
||||
void broadcastMessageUpdated({
|
||||
required int channelId,
|
||||
required Map<String, dynamic> message,
|
||||
}) {
|
||||
_gatewayRegistry.broadcastToChannel(channelId, {
|
||||
'op': 'event',
|
||||
'event': 'message.updated',
|
||||
'channelId': channelId,
|
||||
'message': message,
|
||||
'emittedAt': DateTime.now().toUtc().toIso8601String(),
|
||||
});
|
||||
}
|
||||
|
||||
void broadcastMessageDeleted({
|
||||
required int channelId,
|
||||
required int messageId,
|
||||
}) {
|
||||
_gatewayRegistry.broadcastToChannel(channelId, {
|
||||
'op': 'event',
|
||||
'event': 'message.deleted',
|
||||
'channelId': channelId,
|
||||
'messageId': messageId,
|
||||
'emittedAt': DateTime.now().toUtc().toIso8601String(),
|
||||
});
|
||||
}
|
||||
|
||||
String? _extractGatewayToken(Request req) {
|
||||
final authHeader = req.headers.authorization;
|
||||
if (authHeader is BearerAuthorizationHeader) {
|
||||
final token = authHeader.token.trim();
|
||||
if (token.isNotEmpty) {
|
||||
return token;
|
||||
}
|
||||
}
|
||||
|
||||
final rawAuthorization = req.headers[Headers.authorizationHeader];
|
||||
if (rawAuthorization != null && rawAuthorization.isNotEmpty) {
|
||||
final value = rawAuthorization.first.trim();
|
||||
if (value.toLowerCase().startsWith('bearer ')) {
|
||||
final token = value.substring(7).trim();
|
||||
if (token.isNotEmpty) {
|
||||
return token;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final queryToken = req.url.queryParameters['token']?.trim();
|
||||
if (queryToken != null && queryToken.isNotEmpty) {
|
||||
return queryToken;
|
||||
}
|
||||
|
||||
final queryAccessToken = req.url.queryParameters['access_token']?.trim();
|
||||
if (queryAccessToken != null && queryAccessToken.isNotEmpty) {
|
||||
return queryAccessToken;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
void _handleGatewayTextMessage(String connectionId, String text) {
|
||||
final connection = _gatewayRegistry.getConnection(connectionId);
|
||||
if (connection == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final payload = _decodePayload(text);
|
||||
if (payload == null) {
|
||||
_sendProtocolError(connectionId, 'Invalid JSON payload');
|
||||
return;
|
||||
}
|
||||
|
||||
final rawOp = payload['op'];
|
||||
if (rawOp is! String || rawOp.trim().isEmpty) {
|
||||
_sendProtocolError(connectionId, '`op` is required');
|
||||
return;
|
||||
}
|
||||
|
||||
switch (rawOp.trim().toLowerCase()) {
|
||||
case 'subscribe':
|
||||
_handleSubscribe(connectionId, payload);
|
||||
case 'unsubscribe':
|
||||
_handleUnsubscribe(connectionId, payload);
|
||||
case 'ping':
|
||||
case 'heartbeat':
|
||||
_sendGatewayPayload(connectionId, {
|
||||
'op': 'pong',
|
||||
'ts': DateTime.now().toUtc().toIso8601String(),
|
||||
});
|
||||
case 'pong':
|
||||
return;
|
||||
default:
|
||||
_sendProtocolError(connectionId, 'Unsupported op `$rawOp`');
|
||||
}
|
||||
}
|
||||
|
||||
void _handleSubscribe(String connectionId, Map<String, dynamic> payload) {
|
||||
final channelIds = _parseChannelIds(payload);
|
||||
if (channelIds.isEmpty) {
|
||||
_sendProtocolError(
|
||||
connectionId,
|
||||
'subscribe requires `channelId` or `channelIds`',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final knownChannelIds = <int>[];
|
||||
final unknownChannelIds = <int>[];
|
||||
for (final channelId in channelIds) {
|
||||
if (channelBox.get(channelId) == null) {
|
||||
unknownChannelIds.add(channelId);
|
||||
} else {
|
||||
knownChannelIds.add(channelId);
|
||||
}
|
||||
}
|
||||
|
||||
final subscribed = _gatewayRegistry.subscribe(connectionId, knownChannelIds);
|
||||
|
||||
_sendGatewayPayload(connectionId, {
|
||||
'op': 'subscribed',
|
||||
'channelIds': subscribed,
|
||||
'subscriptions': _gatewayRegistry.subscriptions(connectionId),
|
||||
if (unknownChannelIds.isNotEmpty) 'unknownChannelIds': unknownChannelIds,
|
||||
});
|
||||
}
|
||||
|
||||
void _handleUnsubscribe(String connectionId, Map<String, dynamic> payload) {
|
||||
final channelIds = _parseChannelIds(payload);
|
||||
if (channelIds.isEmpty) {
|
||||
_sendProtocolError(
|
||||
connectionId,
|
||||
'unsubscribe requires `channelId` or `channelIds`',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final unsubscribed = _gatewayRegistry.unsubscribe(connectionId, channelIds);
|
||||
|
||||
_sendGatewayPayload(connectionId, {
|
||||
'op': 'unsubscribed',
|
||||
'channelIds': unsubscribed,
|
||||
'subscriptions': _gatewayRegistry.subscriptions(connectionId),
|
||||
});
|
||||
}
|
||||
|
||||
Map<String, dynamic>? _decodePayload(String text) {
|
||||
try {
|
||||
final decoded = jsonDecode(text);
|
||||
if (decoded is Map<String, dynamic>) {
|
||||
return decoded;
|
||||
}
|
||||
if (decoded is Map) {
|
||||
return decoded.map(
|
||||
(key, value) => MapEntry(key.toString(), value),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Set<int> _parseChannelIds(Map<String, dynamic> payload) {
|
||||
final channelIds = <int>{};
|
||||
|
||||
void parseAndAdd(Object? value) {
|
||||
final parsed = switch (value) {
|
||||
int id when id > 0 => id,
|
||||
String id => int.tryParse(id),
|
||||
_ => null,
|
||||
};
|
||||
if (parsed != null && parsed > 0) {
|
||||
channelIds.add(parsed);
|
||||
}
|
||||
}
|
||||
|
||||
parseAndAdd(payload['channelId']);
|
||||
final channelIdList = payload['channelIds'];
|
||||
if (channelIdList is List) {
|
||||
for (final value in channelIdList) {
|
||||
parseAndAdd(value);
|
||||
}
|
||||
}
|
||||
|
||||
return channelIds;
|
||||
}
|
||||
|
||||
void _sendProtocolError(String connectionId, String message) {
|
||||
_sendGatewayPayload(connectionId, {
|
||||
'op': 'error',
|
||||
'message': message,
|
||||
});
|
||||
}
|
||||
|
||||
void _sendGatewayPayload(String connectionId, Map<String, dynamic> payload) {
|
||||
final connection = _gatewayRegistry.getConnection(connectionId);
|
||||
if (connection == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final sent = _safeSend(connection, payload);
|
||||
if (!sent) {
|
||||
_gatewayRegistry.unregister(connectionId);
|
||||
}
|
||||
}
|
||||
|
||||
bool _safeSend(_GatewayConnection connection, Map<String, dynamic> payload) {
|
||||
try {
|
||||
return connection.socket.trySendText(jsonEncode(payload));
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
class _GatewayRegistry {
|
||||
final Map<String, _GatewayConnection> _connections = {};
|
||||
final Map<int, Set<String>> _subscribersByChannel = {};
|
||||
int _nextConnectionId = 1;
|
||||
|
||||
_GatewayConnection register({
|
||||
required RelicWebSocket socket,
|
||||
required String userId,
|
||||
String? username,
|
||||
}) {
|
||||
final id = 'gw_${_nextConnectionId++}';
|
||||
final connection = _GatewayConnection(
|
||||
id: id,
|
||||
socket: socket,
|
||||
userId: userId,
|
||||
username: username,
|
||||
);
|
||||
_connections[id] = connection;
|
||||
return connection;
|
||||
}
|
||||
|
||||
_GatewayConnection? getConnection(String connectionId) {
|
||||
return _connections[connectionId];
|
||||
}
|
||||
|
||||
List<int> subscriptions(String connectionId) {
|
||||
final connection = _connections[connectionId];
|
||||
if (connection == null) {
|
||||
return const <int>[];
|
||||
}
|
||||
return connection.subscriptions.toList()..sort();
|
||||
}
|
||||
|
||||
List<int> subscribe(String connectionId, Iterable<int> channelIds) {
|
||||
final connection = _connections[connectionId];
|
||||
if (connection == null) {
|
||||
return const <int>[];
|
||||
}
|
||||
|
||||
for (final channelId in channelIds) {
|
||||
connection.subscriptions.add(channelId);
|
||||
_subscribersByChannel.putIfAbsent(channelId, () => <String>{}).add(
|
||||
connectionId,
|
||||
);
|
||||
}
|
||||
|
||||
return channelIds.toSet().toList()..sort();
|
||||
}
|
||||
|
||||
List<int> unsubscribe(String connectionId, Iterable<int> channelIds) {
|
||||
final connection = _connections[connectionId];
|
||||
if (connection == null) {
|
||||
return const <int>[];
|
||||
}
|
||||
|
||||
final removed = <int>[];
|
||||
for (final channelId in channelIds) {
|
||||
final didRemove = connection.subscriptions.remove(channelId);
|
||||
if (didRemove) {
|
||||
removed.add(channelId);
|
||||
}
|
||||
|
||||
final subscribers = _subscribersByChannel[channelId];
|
||||
if (subscribers != null) {
|
||||
subscribers.remove(connectionId);
|
||||
if (subscribers.isEmpty) {
|
||||
_subscribersByChannel.remove(channelId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
removed.sort();
|
||||
return removed;
|
||||
}
|
||||
|
||||
void unregister(String connectionId) {
|
||||
final connection = _connections.remove(connectionId);
|
||||
if (connection == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (final channelId in connection.subscriptions) {
|
||||
final subscribers = _subscribersByChannel[channelId];
|
||||
if (subscribers == null) {
|
||||
continue;
|
||||
}
|
||||
subscribers.remove(connectionId);
|
||||
if (subscribers.isEmpty) {
|
||||
_subscribersByChannel.remove(channelId);
|
||||
}
|
||||
}
|
||||
|
||||
if (!connection.socket.isClosed) {
|
||||
unawaited(connection.socket.tryClose(1000, 'Disconnected'));
|
||||
}
|
||||
}
|
||||
|
||||
void broadcastToChannel(int channelId, Map<String, dynamic> payload) {
|
||||
final subscribers = _subscribersByChannel[channelId];
|
||||
if (subscribers == null || subscribers.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
final deadConnections = <String>[];
|
||||
final subscriberIds = subscribers.toList(growable: false);
|
||||
for (final connectionId in subscriberIds) {
|
||||
final connection = _connections[connectionId];
|
||||
if (connection == null) {
|
||||
deadConnections.add(connectionId);
|
||||
continue;
|
||||
}
|
||||
|
||||
final sent = _safeSend(connection, payload);
|
||||
if (!sent) {
|
||||
deadConnections.add(connectionId);
|
||||
}
|
||||
}
|
||||
|
||||
for (final connectionId in deadConnections) {
|
||||
unregister(connectionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _GatewayConnection {
|
||||
final String id;
|
||||
final RelicWebSocket socket;
|
||||
final String userId;
|
||||
final String? username;
|
||||
final Set<int> subscriptions = <int>{};
|
||||
|
||||
_GatewayConnection({
|
||||
required this.id,
|
||||
required this.socket,
|
||||
required this.userId,
|
||||
required this.username,
|
||||
});
|
||||
}
|
||||
@@ -1,35 +1,11 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:chat_server/models.dart';
|
||||
import 'package:chat_server/objectbox.g.dart';
|
||||
import 'package:relic/relic.dart';
|
||||
|
||||
import '../../config/config.dart';
|
||||
import '../auth/auth_middleware.dart';
|
||||
import '../auth/crypto_service.dart';
|
||||
import '../state/app_state.dart';
|
||||
import '../utils/extensions.dart';
|
||||
|
||||
class RemoteIdentity {
|
||||
final String userId;
|
||||
final String username;
|
||||
final String publicKey;
|
||||
final String? displayName;
|
||||
final String? avatarUrl;
|
||||
final String? bio;
|
||||
|
||||
RemoteIdentity({
|
||||
required this.userId,
|
||||
required this.username,
|
||||
required this.publicKey,
|
||||
this.displayName,
|
||||
this.avatarUrl,
|
||||
this.bio,
|
||||
});
|
||||
}
|
||||
|
||||
/// Register on HOME server
|
||||
/// Creates user with keypair for identity
|
||||
Future<Response> registerHandler(Request req) async {
|
||||
@@ -49,12 +25,8 @@ Future<Response> registerHandler(Request req) async {
|
||||
);
|
||||
}
|
||||
|
||||
// Check if username exists on this home server
|
||||
final query = userBox
|
||||
.query(
|
||||
ChatUser_.username.equals(username) & ChatUser_.isLocal.equals(true),
|
||||
)
|
||||
.build();
|
||||
// Check if username exists
|
||||
final query = userBox.query(User_.username.equals(username)).build();
|
||||
final existing = query.findFirst();
|
||||
query.close();
|
||||
|
||||
@@ -76,30 +48,22 @@ Future<Response> registerHandler(Request req) async {
|
||||
// Hash password
|
||||
final passwordHash = AuthService.hashPassword(password);
|
||||
|
||||
// Create immutable opaque identity ID.
|
||||
final userId = generateUserId();
|
||||
// Create user with userId format: "username@this.server"
|
||||
final userId = '${username}@${Config.serverHost}';
|
||||
|
||||
final isBootstrapAdmin = Config.setupMode && _adminCount() < Config.bootstrapAdminLimit;
|
||||
final roles = isBootstrapAdmin ? <String>['admin'] : <String>['member'];
|
||||
|
||||
final user = ChatUser(
|
||||
final user = User(
|
||||
userId: userId,
|
||||
username: username,
|
||||
publicKey: keyPair.publicKey,
|
||||
isLocal: true,
|
||||
homeServer: Config.publicBaseUrl,
|
||||
passwordHash: passwordHash,
|
||||
encryptedPrivateKey: encryptedKey.encryptedData,
|
||||
keyPairSalt: encryptedKey.salt,
|
||||
roles: roles,
|
||||
roles: ['member'],
|
||||
);
|
||||
|
||||
final localId = userBox.put(user);
|
||||
|
||||
if (isBootstrapAdmin) {
|
||||
print('🔐 Setup mode: granted initial admin to $userId');
|
||||
}
|
||||
|
||||
// Generate token
|
||||
final token = AuthService.generateToken(userId, username);
|
||||
|
||||
@@ -107,14 +71,11 @@ Future<Response> registerHandler(Request req) async {
|
||||
body: {
|
||||
'token': token,
|
||||
'userId': userId,
|
||||
'username': user.username,
|
||||
'handle': toUserHandle(user.username, Config.publicBaseUrl),
|
||||
'username': username,
|
||||
'publicKey': keyPair.publicKey,
|
||||
'encryptedPrivateKey': encryptedKey.encryptedData,
|
||||
'keyPairSalt': encryptedKey.salt,
|
||||
'localId': localId,
|
||||
'roles': roles,
|
||||
'isBootstrapAdmin': isBootstrapAdmin,
|
||||
}.toBody,
|
||||
);
|
||||
}
|
||||
@@ -132,11 +93,7 @@ Future<Response> loginHandler(Request req) async {
|
||||
);
|
||||
}
|
||||
|
||||
final query = userBox
|
||||
.query(
|
||||
ChatUser_.username.equals(username) & ChatUser_.isLocal.equals(true),
|
||||
)
|
||||
.build();
|
||||
final query = userBox.query(User_.username.equals(username)).build();
|
||||
final user = query.findFirst();
|
||||
query.close();
|
||||
|
||||
@@ -168,7 +125,6 @@ Future<Response> loginHandler(Request req) async {
|
||||
'token': token,
|
||||
'userId': user.userId,
|
||||
'username': user.username,
|
||||
'handle': toUserHandle(user.username, user.homeServer ?? Config.publicBaseUrl),
|
||||
'displayName': user.displayName,
|
||||
'avatarUrl': user.avatarUrl,
|
||||
'publicKey': user.publicKey,
|
||||
@@ -183,31 +139,16 @@ Future<Response> loginHandler(Request req) async {
|
||||
/// No password required - proves identity via private key signature
|
||||
Future<Response> joinServerHandler(Request req) async {
|
||||
final body = await req.asJson();
|
||||
final userId = body['userId'] as String?;
|
||||
final username = (body['username'] as String?)?.trim();
|
||||
final homeServer = body['homeServer'] as String?;
|
||||
final userId = body['userId'] as String?; // "alice@homeserver.com"
|
||||
final homeServer = body['homeServer'] as String?; // "https://homeserver.com"
|
||||
final timestamp = body['timestamp'] as int?;
|
||||
final signature = body['signature'] as String?;
|
||||
final username = body['username'] as String?;
|
||||
|
||||
// Validate required fields
|
||||
if (userId == null ||
|
||||
userId.isEmpty ||
|
||||
username == null ||
|
||||
username.isEmpty ||
|
||||
homeServer == null ||
|
||||
homeServer.trim().isEmpty ||
|
||||
timestamp == null ||
|
||||
signature == null) {
|
||||
if (userId == null || homeServer == null || timestamp == null || signature == null) {
|
||||
return Response.badRequest(
|
||||
body: Body.fromString('userId, username, homeServer, timestamp, and signature required'),
|
||||
);
|
||||
}
|
||||
|
||||
final resolvedHomeServer = normalizeHomeServer(homeServer);
|
||||
|
||||
if (resolvedHomeServer == null) {
|
||||
return Response.badRequest(
|
||||
body: Body.fromString('Invalid homeServer URL'),
|
||||
body: Body.fromString('userId, homeServer, timestamp, and signature required'),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -219,51 +160,13 @@ Future<Response> joinServerHandler(Request req) async {
|
||||
);
|
||||
}
|
||||
|
||||
// Fetch current identity from home server
|
||||
final identity = await fetchIdentityFromHomeServer(
|
||||
resolvedHomeServer,
|
||||
username,
|
||||
);
|
||||
|
||||
if (identity == null) {
|
||||
return Response.badRequest(
|
||||
body: Body.fromString('Could not verify identity with home server'),
|
||||
);
|
||||
}
|
||||
|
||||
// Ensure fetched identity matches what caller claims
|
||||
if (identity.userId != userId || identity.username != username) {
|
||||
return Response.forbidden(
|
||||
body: Body.fromString('Identity mismatch from home server'),
|
||||
);
|
||||
}
|
||||
|
||||
// Verify signature
|
||||
// Signature is over: "{userId}:{timestamp}"
|
||||
final messageToVerify = '$userId:$timestamp';
|
||||
final isValid = CryptoService.verify(messageToVerify, signature, identity.publicKey);
|
||||
|
||||
if (!isValid) {
|
||||
return Response.forbidden(
|
||||
body: Body.fromString('Invalid signature'),
|
||||
);
|
||||
}
|
||||
|
||||
// Check if user already exists on this server
|
||||
final existingQuery = userBox.query(ChatUser_.userId.equals(userId)).build();
|
||||
final existingQuery = userBox.query(User_.userId.equals(userId)).build();
|
||||
final existingUser = existingQuery.findFirst();
|
||||
existingQuery.close();
|
||||
|
||||
if (existingUser != null) {
|
||||
existingUser.username = identity.username;
|
||||
existingUser.displayName = identity.displayName;
|
||||
existingUser.avatarUrl = identity.avatarUrl;
|
||||
existingUser.bio = identity.bio;
|
||||
existingUser.publicKey = identity.publicKey;
|
||||
existingUser.homeServer = resolvedHomeServer;
|
||||
existingUser.lastSyncedAt = DateTime.now();
|
||||
userBox.put(existingUser);
|
||||
|
||||
// User already joined - just return a new token
|
||||
final token = AuthService.generateToken(existingUser.userId, existingUser.username);
|
||||
userLocalIdProperty[req] = existingUser.id;
|
||||
|
||||
@@ -272,25 +175,40 @@ Future<Response> joinServerHandler(Request req) async {
|
||||
'token': token,
|
||||
'userId': existingUser.userId,
|
||||
'username': existingUser.username,
|
||||
'handle': toUserHandle(existingUser.username, existingUser.homeServer ?? Config.publicBaseUrl),
|
||||
'localId': existingUser.id,
|
||||
'alreadyJoined': true,
|
||||
}.toBody,
|
||||
);
|
||||
}
|
||||
|
||||
// Fetch public key from home server
|
||||
final publicKey = await fetchPublicKeyFromHomeServer(homeServer, username ?? userId.split('@').first);
|
||||
|
||||
if (publicKey == null) {
|
||||
return Response.badRequest(
|
||||
body: Body.fromString('Could not verify identity with home server'),
|
||||
);
|
||||
}
|
||||
|
||||
// Verify signature
|
||||
// Signature is over: "{userId}:{timestamp}"
|
||||
final messageToVerify = '$userId:$timestamp';
|
||||
final isValid = CryptoService.verify(messageToVerify, signature, publicKey);
|
||||
|
||||
if (!isValid) {
|
||||
return Response.forbidden(
|
||||
body: Body.fromString('Invalid signature'),
|
||||
);
|
||||
}
|
||||
|
||||
// Create local shadow account
|
||||
final user = ChatUser(
|
||||
final user = User(
|
||||
userId: userId,
|
||||
username: identity.username,
|
||||
displayName: identity.displayName,
|
||||
avatarUrl: identity.avatarUrl,
|
||||
bio: identity.bio,
|
||||
publicKey: identity.publicKey,
|
||||
username: username ?? userId.split('@').first,
|
||||
publicKey: publicKey,
|
||||
isLocal: false,
|
||||
homeServer: resolvedHomeServer,
|
||||
homeServer: homeServer,
|
||||
roles: ['member'],
|
||||
lastSyncedAt: DateTime.now(),
|
||||
);
|
||||
|
||||
final localId = userBox.put(user);
|
||||
@@ -303,108 +221,43 @@ Future<Response> joinServerHandler(Request req) async {
|
||||
'token': token,
|
||||
'userId': userId,
|
||||
'username': user.username,
|
||||
'handle': toUserHandle(user.username, user.homeServer ?? Config.publicBaseUrl),
|
||||
'localId': localId,
|
||||
'alreadyJoined': false,
|
||||
}.toBody,
|
||||
);
|
||||
}
|
||||
|
||||
/// Fetch public identity from user's home server
|
||||
Future<RemoteIdentity?> fetchIdentityFromHomeServer(
|
||||
/// Fetch public key from user's home server
|
||||
Future<String?> fetchPublicKeyFromHomeServer(
|
||||
String homeServer,
|
||||
String username,
|
||||
) async {
|
||||
final client = HttpClient();
|
||||
client.connectionTimeout = const Duration(seconds: 5);
|
||||
|
||||
try {
|
||||
// Remove trailing slash
|
||||
final baseUrl = homeServer.replaceAll(RegExp(r'/$'), '');
|
||||
final endpoint = Uri.parse('$baseUrl/.well-known/users/${Uri.encodeComponent(username)}');
|
||||
|
||||
final request = await client.getUrl(endpoint);
|
||||
request.headers.set(HttpHeaders.acceptHeader, 'application/json');
|
||||
final response = await request.close();
|
||||
// Use http client to fetch
|
||||
// Endpoint: GET /.well-known/users/{username}
|
||||
// Returns: { publicKey, userId, ... }
|
||||
|
||||
if (response.statusCode != HttpStatus.ok) {
|
||||
return null;
|
||||
}
|
||||
// For now, return null to indicate we need http client
|
||||
// This will be implemented with actual HTTP client
|
||||
print('Would fetch: $baseUrl/.well-known/users/$username');
|
||||
|
||||
final responseBody = await utf8.decodeStream(response);
|
||||
final dynamic decoded = jsonDecode(responseBody);
|
||||
if (decoded is! Map) {
|
||||
return null;
|
||||
}
|
||||
// TODO: Implement actual HTTP request
|
||||
// final response = await http.get(Uri.parse('$baseUrl/.well-known/users/$username'));
|
||||
// if (response.statusCode == 200) {
|
||||
// final data = jsonDecode(response.body);
|
||||
// return data['publicKey'] as String;
|
||||
// }
|
||||
|
||||
final data = Map<String, dynamic>.from(decoded);
|
||||
final userId = data['userId'] as String?;
|
||||
final fetchedUsername = data['username'] as String?;
|
||||
final publicKey = data['publicKey'] as String?;
|
||||
|
||||
if (userId == null || fetchedUsername == null || publicKey == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return RemoteIdentity(
|
||||
userId: userId,
|
||||
username: fetchedUsername,
|
||||
publicKey: publicKey,
|
||||
displayName: data['displayName'] as String?,
|
||||
avatarUrl: data['avatarUrl'] as String?,
|
||||
bio: data['bio'] as String?,
|
||||
);
|
||||
return null;
|
||||
} catch (e) {
|
||||
print('Error fetching public key: $e');
|
||||
return null;
|
||||
} finally {
|
||||
client.close(force: true);
|
||||
}
|
||||
}
|
||||
|
||||
String? normalizeHomeServer(String homeServerOrHost) {
|
||||
final trimmed = homeServerOrHost.trim();
|
||||
if (trimmed.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final hasScheme = trimmed.contains('://');
|
||||
final candidate = hasScheme ? trimmed : '${defaultSchemeForHost(trimmed)}://$trimmed';
|
||||
final uri = Uri.tryParse(candidate);
|
||||
|
||||
if (uri == null || uri.host.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final normalized = Uri(
|
||||
scheme: uri.scheme,
|
||||
userInfo: uri.userInfo,
|
||||
host: uri.host,
|
||||
port: uri.hasPort ? uri.port : null,
|
||||
).toString();
|
||||
|
||||
return normalized.replaceAll(RegExp(r'/$'), '');
|
||||
}
|
||||
|
||||
String defaultSchemeForHost(String host) {
|
||||
final lowerHost = host.toLowerCase();
|
||||
if (lowerHost.startsWith('localhost') || lowerHost.startsWith('127.') || lowerHost.startsWith('[::1]')) {
|
||||
return 'http';
|
||||
}
|
||||
|
||||
return 'https';
|
||||
}
|
||||
|
||||
int _adminCount() {
|
||||
var count = 0;
|
||||
final users = userBox.getAll();
|
||||
for (final user in users) {
|
||||
if (user.roles.contains('admin')) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/// Get current user's profile
|
||||
Future<Response> getMeHandler(Request req) async {
|
||||
final userId = userIdProperty[req];
|
||||
@@ -415,7 +268,7 @@ Future<Response> getMeHandler(Request req) async {
|
||||
);
|
||||
}
|
||||
|
||||
final query = userBox.query(ChatUser_.userId.equals(userId)).build();
|
||||
final query = userBox.query(User_.userId.equals(userId)).build();
|
||||
final user = query.findFirst();
|
||||
query.close();
|
||||
|
||||
@@ -429,7 +282,6 @@ Future<Response> getMeHandler(Request req) async {
|
||||
body: {
|
||||
'userId': user.userId,
|
||||
'username': user.username,
|
||||
'handle': toUserHandle(user.username, user.homeServer ?? Config.publicBaseUrl),
|
||||
'displayName': user.displayName,
|
||||
'avatarUrl': user.avatarUrl,
|
||||
'bio': user.bio,
|
||||
@@ -440,27 +292,3 @@ Future<Response> getMeHandler(Request req) async {
|
||||
}.toBody,
|
||||
);
|
||||
}
|
||||
|
||||
String generateUserId() {
|
||||
const alphabet = '0123456789abcdefghijklmnopqrstuvwxyz';
|
||||
final random = Random.secure();
|
||||
while (true) {
|
||||
final bytes = List<int>.generate(16, (_) => random.nextInt(256));
|
||||
final id = bytes.map((b) => alphabet[b % alphabet.length]).join();
|
||||
final query = userBox.query(ChatUser_.userId.equals(id)).build();
|
||||
final exists = query.findFirst() != null;
|
||||
query.close();
|
||||
if (!exists) {
|
||||
return id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String toUserHandle(String username, String homeServer) {
|
||||
final uri = Uri.tryParse(homeServer);
|
||||
if (uri != null && uri.host.isNotEmpty) {
|
||||
final host = uri.hasPort ? '${uri.host}:${uri.port}' : uri.host;
|
||||
return '@$username@$host';
|
||||
}
|
||||
return '@$username@${homeServer.trim()}';
|
||||
}
|
||||
@@ -2,7 +2,6 @@ import 'package:chat_server/models.dart';
|
||||
import 'package:chat_server/objectbox.g.dart';
|
||||
import 'package:relic/relic.dart';
|
||||
|
||||
import '../state/app_state.dart';
|
||||
import '../utils/extensions.dart';
|
||||
|
||||
/// Get all channels
|
||||
@@ -26,8 +25,7 @@ Future<Response> getChannelsHandler(Request req) async {
|
||||
/// Create a channel (requires 'admin' role)
|
||||
Future<Response> createChannelHandler(Request req) async {
|
||||
final body = await req.asJson();
|
||||
final name =
|
||||
(body['name'] as String?)?.trim().toLowerCase().replaceAll(' ', '-');
|
||||
final name = (body['name'] as String?)?.trim().toLowerCase().replaceAll(' ', '-');
|
||||
final description = body['description'] as String?;
|
||||
|
||||
if (name == null || name.isEmpty) {
|
||||
@@ -37,15 +35,14 @@ Future<Response> createChannelHandler(Request req) async {
|
||||
}
|
||||
|
||||
// Check if channel exists
|
||||
final existing =
|
||||
channelBox.query(ChatChannel_.name.equals(name)).build().findFirst();
|
||||
final existing = channelBox.query(Channel_.name.equals(name)).build().findFirst();
|
||||
if (existing != null) {
|
||||
return Response.badRequest(
|
||||
body: Body.fromString('Channel already exists'),
|
||||
);
|
||||
}
|
||||
|
||||
final channel = ChatChannel(name: name, description: description);
|
||||
final channel = Channel(name: name, description: description);
|
||||
final id = channelBox.put(channel);
|
||||
|
||||
return Response.ok(
|
||||
@@ -59,14 +56,7 @@ Future<Response> createChannelHandler(Request req) async {
|
||||
}
|
||||
|
||||
/// Get channel by ID
|
||||
Future<Response> getChannelHandler(Request req) async {
|
||||
final channelIdStr = req.rawPathParameters[#channelId];
|
||||
if (channelIdStr == null) {
|
||||
return Response.badRequest(
|
||||
body: Body.fromString('Invalid channel ID'),
|
||||
);
|
||||
}
|
||||
|
||||
Future<Response> getChannelHandler(Request req, String channelIdStr) async {
|
||||
final channelId = int.tryParse(channelIdStr);
|
||||
if (channelId == null) {
|
||||
return Response.badRequest(
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import 'package:chat_server/objectbox.g.dart';
|
||||
import 'package:relic/relic.dart';
|
||||
|
||||
import '../utils/extensions.dart';
|
||||
|
||||
Future<Response> debugUsersHandler(Request req) async {
|
||||
return Response.ok(
|
||||
body: {
|
||||
'users': userBox.getAll().map((u) => {'id': u.id, 'username': u.username}).toList(),
|
||||
}.toBody,
|
||||
);
|
||||
}
|
||||
|
||||
Future<Response> debugMessagesHandler(Request req) async {
|
||||
return Response.ok(
|
||||
body: {
|
||||
'messages': messageBox
|
||||
.getAll()
|
||||
.map(
|
||||
(m) => {
|
||||
'id': m.id,
|
||||
'content': m.content,
|
||||
'senderId': m.senderId,
|
||||
'channelId': m.channelId,
|
||||
'createdAt': m.createdAt.toIso8601String(),
|
||||
},
|
||||
)
|
||||
.toList(),
|
||||
}.toBody,
|
||||
);
|
||||
}
|
||||
|
||||
Future<Response> debugClearHandler(Request req) async {
|
||||
messageBox.removeAll();
|
||||
channelBox.removeAll();
|
||||
userBox.removeAll();
|
||||
return Response.ok(body: {'cleared': true}.toBody);
|
||||
}
|
||||
@@ -1,280 +0,0 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:chat_server/models.dart';
|
||||
import 'package:chat_server/objectbox.g.dart';
|
||||
import 'package:relic/relic.dart';
|
||||
|
||||
import '../../config/config.dart';
|
||||
import '../auth/auth_middleware.dart';
|
||||
import '../state/app_state.dart';
|
||||
import '../utils/extensions.dart';
|
||||
|
||||
const _maxSnapshotLimit = 500;
|
||||
|
||||
Future<Response> getDevDbSnapshotHandler(Request req) async {
|
||||
final guard = _guardDevDbTools(req);
|
||||
if (guard != null) {
|
||||
return guard;
|
||||
}
|
||||
|
||||
final rawLimit = req.url.queryParameters['limit'];
|
||||
final parsedLimit = rawLimit == null ? 100 : int.tryParse(rawLimit);
|
||||
if (parsedLimit == null || parsedLimit <= 0) {
|
||||
return Response.badRequest(
|
||||
body: Body.fromString('Invalid limit'),
|
||||
);
|
||||
}
|
||||
final limit = parsedLimit > _maxSnapshotLimit ? _maxSnapshotLimit : parsedLimit;
|
||||
|
||||
final users = userBox.getAll().take(limit).map(_userToMap).toList();
|
||||
final channels = channelBox.getAll().take(limit).map(_channelToMap).toList();
|
||||
final messages = messageBox.getAll().take(limit).map(_messageToMap).toList();
|
||||
final files = fileBox.getAll().take(limit).map(_fileToMap).toList();
|
||||
final messageMedia = messageMediaBox.getAll().take(limit).map(_messageMediaToMap).toList();
|
||||
final settings = serverSettingsBox.getAll().take(limit).map(_settingsToMap).toList();
|
||||
|
||||
return Response.ok(
|
||||
body: {
|
||||
'limit': limit,
|
||||
'counts': {
|
||||
'users': userBox.count(),
|
||||
'channels': channelBox.count(),
|
||||
'messages': messageBox.count(),
|
||||
'files': fileBox.count(),
|
||||
'messageMedia': messageMediaBox.count(),
|
||||
'serverSettings': serverSettingsBox.count(),
|
||||
},
|
||||
'users': users,
|
||||
'channels': channels,
|
||||
'messages': messages,
|
||||
'files': files,
|
||||
'messageMedia': messageMedia,
|
||||
'serverSettings': settings,
|
||||
}.toBody,
|
||||
);
|
||||
}
|
||||
|
||||
Future<Response> resetDevDbHandler(Request req) async {
|
||||
final guard = _guardDevDbTools(req);
|
||||
if (guard != null) {
|
||||
return guard;
|
||||
}
|
||||
|
||||
final body = await req.asJson();
|
||||
if ((body['confirm'] as String?) != 'RESET') {
|
||||
return Response.badRequest(
|
||||
body: Body.fromString('Body must include {"confirm":"RESET"}'),
|
||||
);
|
||||
}
|
||||
|
||||
messageBox.removeAll();
|
||||
channelBox.removeAll();
|
||||
fileBox.removeAll();
|
||||
messageMediaBox.removeAll();
|
||||
serverSettingsBox.removeAll();
|
||||
userBox.removeAll();
|
||||
|
||||
final uploadsDir = Directory('uploads');
|
||||
if (uploadsDir.existsSync()) {
|
||||
for (final entity in uploadsDir.listSync()) {
|
||||
if (entity is File) {
|
||||
entity.deleteSync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Response.ok(
|
||||
body: {
|
||||
'reset': true,
|
||||
'counts': {
|
||||
'users': userBox.count(),
|
||||
'channels': channelBox.count(),
|
||||
'messages': messageBox.count(),
|
||||
'files': fileBox.count(),
|
||||
'messageMedia': messageMediaBox.count(),
|
||||
'serverSettings': serverSettingsBox.count(),
|
||||
},
|
||||
}.toBody,
|
||||
);
|
||||
}
|
||||
|
||||
Future<Response> promoteUserToAdminHandler(Request req) async {
|
||||
final guard = _guardDevDbTools(req);
|
||||
if (guard != null) {
|
||||
return guard;
|
||||
}
|
||||
|
||||
final body = await req.asJson();
|
||||
final userId = (body['userId'] as String?)?.trim();
|
||||
if (userId == null || userId.isEmpty) {
|
||||
return Response.badRequest(
|
||||
body: Body.fromString('userId is required'),
|
||||
);
|
||||
}
|
||||
|
||||
final query = userBox.query(ChatUser_.userId.equals(userId)).build();
|
||||
final user = query.findFirst();
|
||||
query.close();
|
||||
if (user == null) {
|
||||
return Response.notFound(
|
||||
body: Body.fromString('User not found'),
|
||||
);
|
||||
}
|
||||
|
||||
if (!user.roles.contains('admin')) {
|
||||
user.roles = [...user.roles, 'admin'];
|
||||
userBox.put(user);
|
||||
}
|
||||
|
||||
return Response.ok(
|
||||
body: {
|
||||
'updated': true,
|
||||
'userId': user.userId,
|
||||
'roles': user.roles,
|
||||
}.toBody,
|
||||
);
|
||||
}
|
||||
|
||||
Future<Response> demoteUserFromAdminHandler(Request req) async {
|
||||
final guard = _guardDevDbTools(req);
|
||||
if (guard != null) {
|
||||
return guard;
|
||||
}
|
||||
|
||||
final body = await req.asJson();
|
||||
final userId = (body['userId'] as String?)?.trim();
|
||||
if (userId == null || userId.isEmpty) {
|
||||
return Response.badRequest(
|
||||
body: Body.fromString('userId is required'),
|
||||
);
|
||||
}
|
||||
|
||||
final query = userBox.query(ChatUser_.userId.equals(userId)).build();
|
||||
final user = query.findFirst();
|
||||
query.close();
|
||||
if (user == null) {
|
||||
return Response.notFound(
|
||||
body: Body.fromString('User not found'),
|
||||
);
|
||||
}
|
||||
|
||||
user.roles = user.roles.where((role) => role != 'admin').toList();
|
||||
userBox.put(user);
|
||||
|
||||
return Response.ok(
|
||||
body: {
|
||||
'updated': true,
|
||||
'userId': user.userId,
|
||||
'roles': user.roles,
|
||||
}.toBody,
|
||||
);
|
||||
}
|
||||
|
||||
Response? _guardDevDbTools(Request req) {
|
||||
if (!Config.devDbTools) {
|
||||
return Response.forbidden(
|
||||
body: Body.fromString('Dev DB tools are disabled in config'),
|
||||
);
|
||||
}
|
||||
|
||||
final authUserId = userIdProperty[req];
|
||||
if (authUserId == null) {
|
||||
return Response.unauthorized(
|
||||
body: Body.fromString('Not authenticated'),
|
||||
);
|
||||
}
|
||||
|
||||
final query = userBox.query(ChatUser_.userId.equals(authUserId)).build();
|
||||
final user = query.findFirst();
|
||||
query.close();
|
||||
if (user == null) {
|
||||
return Response.unauthorized(
|
||||
body: Body.fromString('User not found'),
|
||||
);
|
||||
}
|
||||
if (!user.roles.contains('admin')) {
|
||||
return Response.forbidden(
|
||||
body: Body.fromString('Admin role required'),
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
Map<String, dynamic> _userToMap(ChatUser user) => {
|
||||
'id': user.id,
|
||||
'userId': user.userId,
|
||||
'username': user.username,
|
||||
'displayName': user.displayName,
|
||||
'avatarUrl': user.avatarUrl,
|
||||
'bio': user.bio,
|
||||
'isLocal': user.isLocal,
|
||||
'homeServer': user.homeServer,
|
||||
'roles': user.roles,
|
||||
'createdAt': user.createdAt.toIso8601String(),
|
||||
'lastSyncedAt': user.lastSyncedAt?.toIso8601String(),
|
||||
};
|
||||
|
||||
Map<String, dynamic> _channelToMap(ChatChannel channel) => {
|
||||
'id': channel.id,
|
||||
'name': channel.name,
|
||||
'description': channel.description,
|
||||
'createdAt': channel.createdAt.toIso8601String(),
|
||||
};
|
||||
|
||||
Map<String, dynamic> _messageToMap(ChatMessage message) => {
|
||||
'id': message.id,
|
||||
'channelId': message.channelId,
|
||||
'authorLocalId': message.authorLocalId,
|
||||
'authorUserId': message.authorUserId,
|
||||
'authorUsername': message.authorUsername,
|
||||
'content': message.content,
|
||||
'componentsJson': message.componentsJson,
|
||||
'createdAt': message.createdAt.toIso8601String(),
|
||||
'editedAt': message.editedAt?.toIso8601String(),
|
||||
};
|
||||
|
||||
Map<String, dynamic> _fileToMap(ChatFile file) => {
|
||||
'id': file.id,
|
||||
'attachmentId': file.attachmentId,
|
||||
'uploaderId': file.uploaderId,
|
||||
'path': file.path,
|
||||
'originalFileName': file.originalFileName,
|
||||
'storedFileName': file.storedFileName,
|
||||
'mimeType': file.mimeType,
|
||||
'sizeBytes': file.sizeBytes,
|
||||
'width': file.width,
|
||||
'height': file.height,
|
||||
'usageType': file.usageType,
|
||||
'status': file.status,
|
||||
'isAttached': file.isAttached,
|
||||
'createdAt': file.createdAt.toIso8601String(),
|
||||
};
|
||||
|
||||
Map<String, dynamic> _settingsToMap(ServerSettings settings) => {
|
||||
'id': settings.id,
|
||||
'singletonKey': settings.singletonKey,
|
||||
'serverName': settings.serverName,
|
||||
'description': settings.description,
|
||||
'iconAttachmentId': settings.iconAttachmentId,
|
||||
'bannerAttachmentId': settings.bannerAttachmentId,
|
||||
'updatedByUserId': settings.updatedByUserId,
|
||||
'updatedAt': settings.updatedAt.toIso8601String(),
|
||||
};
|
||||
|
||||
Map<String, dynamic> _messageMediaToMap(ChatMessageMedia media) => {
|
||||
'id': media.id,
|
||||
'messageId': media.messageId,
|
||||
'channelId': media.channelId,
|
||||
'authorUserId': media.authorUserId,
|
||||
'attachmentId': media.attachmentId,
|
||||
'externalUrl': media.externalUrl,
|
||||
'kind': media.kind,
|
||||
'mimeType': media.mimeType,
|
||||
'originalFileName': media.originalFileName,
|
||||
'sizeBytes': media.sizeBytes,
|
||||
'width': media.width,
|
||||
'height': media.height,
|
||||
'thumbHash': media.thumbHash,
|
||||
'createdAt': media.createdAt.toIso8601String(),
|
||||
};
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,29 +1,18 @@
|
||||
import 'package:chat_server/models.dart';
|
||||
import 'package:chat_server/objectbox.g.dart';
|
||||
import 'package:relic/relic.dart';
|
||||
|
||||
import '../../config/config.dart';
|
||||
import 'auth_handlers.dart';
|
||||
import '../state/app_state.dart';
|
||||
import '../utils/extensions.dart';
|
||||
|
||||
/// Public endpoint for cross-server identity verification
|
||||
/// Called by other servers to verify a user's public key
|
||||
///
|
||||
///
|
||||
/// GET /.well-known/users/{username}
|
||||
/// Returns: { userId, publicKey, username, displayName, avatarUrl }
|
||||
Future<Response> publicUserHandler(Request req) async {
|
||||
final username = req.rawPathParameters[#username];
|
||||
if (username == null) {
|
||||
return Response.badRequest(
|
||||
body: Body.fromString('Username required'),
|
||||
);
|
||||
}
|
||||
|
||||
final query = userBox
|
||||
.query(
|
||||
ChatUser_.username.equals(username) & ChatUser_.isLocal.equals(true),
|
||||
)
|
||||
.build();
|
||||
Future<Response> publicUserHandler(Request req, String username) async {
|
||||
final query = userBox.query(
|
||||
User_.username.equals(username) & User_.isLocal.equals(true),
|
||||
).build();
|
||||
|
||||
final user = query.findFirst();
|
||||
query.close();
|
||||
@@ -39,11 +28,10 @@ Future<Response> publicUserHandler(Request req) async {
|
||||
'userId': user.userId,
|
||||
'username': user.username,
|
||||
'displayName': user.displayName,
|
||||
'handle': toUserHandle(user.username, user.homeServer ?? Config.publicBaseUrl),
|
||||
'avatarUrl': user.avatarUrl,
|
||||
'bio': user.bio,
|
||||
'publicKey': user.publicKey,
|
||||
'homeServer': user.homeServer ?? Config.publicBaseUrl,
|
||||
'homeServer': Config.serverHost,
|
||||
'createdAt': user.createdAt.toIso8601String(),
|
||||
}.toBody,
|
||||
);
|
||||
@@ -51,22 +39,13 @@ Future<Response> publicUserHandler(Request req) async {
|
||||
|
||||
/// Public endpoint to verify a user exists
|
||||
/// Lightweight check without full profile
|
||||
///
|
||||
///
|
||||
/// GET /.well-known/verify/{userId}
|
||||
/// Returns: { exists, userId, publicKey }
|
||||
Future<Response> publicVerifyHandler(Request req) async {
|
||||
final userId = req.rawPathParameters[#userId];
|
||||
if (userId == null) {
|
||||
return Response.badRequest(
|
||||
body: Body.fromString('userId required'),
|
||||
);
|
||||
}
|
||||
|
||||
final query = userBox
|
||||
.query(
|
||||
ChatUser_.userId.equals(userId) & ChatUser_.isLocal.equals(true),
|
||||
)
|
||||
.build();
|
||||
Future<Response> publicVerifyHandler(Request req, String userId) async {
|
||||
final query = userBox.query(
|
||||
User_.userId.equals(userId) & User_.isLocal.equals(true),
|
||||
).build();
|
||||
|
||||
final user = query.findFirst();
|
||||
query.close();
|
||||
@@ -86,28 +65,20 @@ Future<Response> publicVerifyHandler(Request req) async {
|
||||
'userId': user.userId,
|
||||
'publicKey': user.publicKey,
|
||||
'username': user.username,
|
||||
'handle': toUserHandle(user.username, user.homeServer ?? Config.publicBaseUrl),
|
||||
}.toBody,
|
||||
);
|
||||
}
|
||||
|
||||
/// Public server info
|
||||
///
|
||||
///
|
||||
/// GET /.well-known/server
|
||||
/// Returns: { serverHost, serverName, version }
|
||||
Future<Response> publicServerInfoHandler(Request req) async {
|
||||
final settingsQuery = serverSettingsBox.query(ServerSettings_.singletonKey.equals('default')).build();
|
||||
final settings = settingsQuery.findFirst();
|
||||
settingsQuery.close();
|
||||
|
||||
return Response.ok(
|
||||
body: {
|
||||
'serverHost': Config.publicHost,
|
||||
'publicBaseUrl': Config.publicBaseUrl,
|
||||
'serverName': settings?.serverName ?? Config.serverName,
|
||||
'serverHost': Config.serverHost,
|
||||
'serverName': Config.serverName,
|
||||
'version': '0.1.0',
|
||||
'iconUrl': settings?.iconAttachmentId == null ? null : '/icons/${Uri.encodeComponent(Config.publicHost)}',
|
||||
'bannerUrl': settings?.bannerAttachmentId == null ? null : '/banners/${Uri.encodeComponent(Config.publicHost)}',
|
||||
'features': ['identity', 'chat', 'sticker-packs'],
|
||||
}.toBody,
|
||||
);
|
||||
|
||||
@@ -1,348 +0,0 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:chat_server/models.dart';
|
||||
import 'package:chat_server/objectbox.g.dart';
|
||||
import 'package:relic/relic.dart';
|
||||
|
||||
import '../../config/config.dart';
|
||||
import '../auth/auth_middleware.dart';
|
||||
import '../state/app_state.dart';
|
||||
import '../utils/extensions.dart';
|
||||
|
||||
Future<Response> getServerSettingsHandler(Request req) async {
|
||||
final auth = _requireCurrentUser(req);
|
||||
if (auth.$2 != null) {
|
||||
return auth.$2!;
|
||||
}
|
||||
|
||||
final settings = _ensureServerSettings();
|
||||
return Response.ok(
|
||||
body: _serverSettingsToMap(settings).toBody,
|
||||
);
|
||||
}
|
||||
|
||||
Future<Response> patchServerSettingsHandler(Request req) async {
|
||||
final auth = _requireCurrentUser(req);
|
||||
if (auth.$2 != null) {
|
||||
return auth.$2!;
|
||||
}
|
||||
final user = auth.$1!;
|
||||
if (!_isAdmin(user)) {
|
||||
return Response.forbidden(
|
||||
body: Body.fromString('Admin role required'),
|
||||
);
|
||||
}
|
||||
|
||||
final body = await req.asJson();
|
||||
final settings = _ensureServerSettings();
|
||||
|
||||
final nextServerName = (body['serverName'] as String?)?.trim();
|
||||
final nextDescription = (body['description'] as String?)?.trim();
|
||||
|
||||
if (nextServerName != null && nextServerName.isNotEmpty) {
|
||||
settings.serverName = nextServerName;
|
||||
}
|
||||
if (body.containsKey('description')) {
|
||||
settings.description = nextDescription != null && nextDescription.isNotEmpty ? nextDescription : null;
|
||||
}
|
||||
|
||||
settings.updatedByUserId = user.userId;
|
||||
settings.updatedAt = DateTime.now();
|
||||
serverSettingsBox.put(settings);
|
||||
|
||||
return Response.ok(
|
||||
body: _serverSettingsToMap(settings).toBody,
|
||||
);
|
||||
}
|
||||
|
||||
Future<Response> updateProfileHandler(Request req) async {
|
||||
final auth = _requireCurrentUser(req);
|
||||
if (auth.$2 != null) {
|
||||
return auth.$2!;
|
||||
}
|
||||
final user = auth.$1!;
|
||||
|
||||
final body = await req.asJson();
|
||||
final displayName = (body['displayName'] as String?)?.trim();
|
||||
final bio = (body['bio'] as String?)?.trim();
|
||||
|
||||
if (body.containsKey('displayName')) {
|
||||
user.displayName = displayName != null && displayName.isNotEmpty ? displayName : null;
|
||||
}
|
||||
if (body.containsKey('bio')) {
|
||||
user.bio = bio != null && bio.isNotEmpty ? bio : null;
|
||||
}
|
||||
|
||||
userBox.put(user);
|
||||
|
||||
return Response.ok(
|
||||
body: {
|
||||
'userId': user.userId,
|
||||
'username': user.username,
|
||||
'displayName': user.displayName,
|
||||
'avatarUrl': user.avatarUrl,
|
||||
'bio': user.bio,
|
||||
'roles': user.roles,
|
||||
}.toBody,
|
||||
);
|
||||
}
|
||||
|
||||
Future<Response> setAvatarHandler(Request req) async {
|
||||
final auth = _requireCurrentUser(req);
|
||||
if (auth.$2 != null) {
|
||||
return auth.$2!;
|
||||
}
|
||||
final user = auth.$1!;
|
||||
|
||||
final body = await req.asJson();
|
||||
final attachmentId = (body['attachmentId'] as String?)?.trim();
|
||||
if (attachmentId == null || attachmentId.isEmpty) {
|
||||
return Response.badRequest(
|
||||
body: Body.fromString('attachmentId is required'),
|
||||
);
|
||||
}
|
||||
|
||||
final query = fileBox.query(ChatFile_.attachmentId.equals(attachmentId)).build();
|
||||
final file = query.findFirst();
|
||||
query.close();
|
||||
if (file == null || file.status != 'ready') {
|
||||
return Response.notFound(
|
||||
body: Body.fromString('Attachment not found or not ready'),
|
||||
);
|
||||
}
|
||||
if (file.uploaderId != user.id) {
|
||||
return Response.forbidden(
|
||||
body: Body.fromString('Attachment must be uploaded by current user'),
|
||||
);
|
||||
}
|
||||
|
||||
file.usageType = 'avatar';
|
||||
fileBox.put(file);
|
||||
|
||||
user.avatarUrl = '/avatars/${Uri.encodeComponent(user.userId)}';
|
||||
userBox.put(user);
|
||||
|
||||
return Response.ok(
|
||||
body: {
|
||||
'userId': user.userId,
|
||||
'avatarAttachmentId': file.attachmentId,
|
||||
'avatarUrl': user.avatarUrl,
|
||||
}.toBody,
|
||||
);
|
||||
}
|
||||
|
||||
Future<Response> setServerIconHandler(Request req) async {
|
||||
return _setServerMedia(req, usageType: 'server_icon', target: 'icon');
|
||||
}
|
||||
|
||||
Future<Response> setServerBannerHandler(Request req) async {
|
||||
return _setServerMedia(req, usageType: 'server_banner', target: 'banner');
|
||||
}
|
||||
|
||||
Future<Response> getAvatarAliasHandler(Request req) async {
|
||||
final userId = req.rawPathParameters[#userId];
|
||||
if (userId == null || userId.isEmpty) {
|
||||
return Response.badRequest(
|
||||
body: Body.fromString('userId is required'),
|
||||
);
|
||||
}
|
||||
|
||||
final query = userBox.query(ChatUser_.userId.equals(userId)).build();
|
||||
final user = query.findFirst();
|
||||
query.close();
|
||||
if (user == null || user.avatarUrl == null) {
|
||||
return Response.notFound(
|
||||
body: Body.fromString('Avatar not found'),
|
||||
);
|
||||
}
|
||||
|
||||
final avatarQuery = fileBox
|
||||
.query(
|
||||
ChatFile_.uploaderId.equals(user.id) & ChatFile_.usageType.equals('avatar') & ChatFile_.status.equals('ready'),
|
||||
)
|
||||
.order(ChatFile_.id, flags: Order.descending)
|
||||
.build();
|
||||
final avatar = avatarQuery.findFirst();
|
||||
avatarQuery.close();
|
||||
if (avatar == null) {
|
||||
return Response.notFound(
|
||||
body: Body.fromString('Avatar not found'),
|
||||
);
|
||||
}
|
||||
return _serveAttachment(avatar.attachmentId, mutableAlias: true);
|
||||
}
|
||||
|
||||
Future<Response> getServerIconAliasHandler(Request req) async {
|
||||
final serverId = req.rawPathParameters[#serverId];
|
||||
if (!_matchesCurrentServer(serverId)) {
|
||||
return Response.notFound(
|
||||
body: Body.fromString('Server not found'),
|
||||
);
|
||||
}
|
||||
final settings = _ensureServerSettings();
|
||||
final attachmentId = settings.iconAttachmentId;
|
||||
if (attachmentId == null || attachmentId.isEmpty) {
|
||||
return Response.notFound(
|
||||
body: Body.fromString('Server icon not found'),
|
||||
);
|
||||
}
|
||||
return _serveAttachment(attachmentId, mutableAlias: true);
|
||||
}
|
||||
|
||||
Future<Response> getServerBannerAliasHandler(Request req) async {
|
||||
final serverId = req.rawPathParameters[#serverId];
|
||||
if (!_matchesCurrentServer(serverId)) {
|
||||
return Response.notFound(
|
||||
body: Body.fromString('Server not found'),
|
||||
);
|
||||
}
|
||||
final settings = _ensureServerSettings();
|
||||
final attachmentId = settings.bannerAttachmentId;
|
||||
if (attachmentId == null || attachmentId.isEmpty) {
|
||||
return Response.notFound(
|
||||
body: Body.fromString('Server banner not found'),
|
||||
);
|
||||
}
|
||||
return _serveAttachment(attachmentId, mutableAlias: true);
|
||||
}
|
||||
|
||||
Future<Response> _setServerMedia(
|
||||
Request req, {
|
||||
required String usageType,
|
||||
required String target,
|
||||
}) async {
|
||||
final auth = _requireCurrentUser(req);
|
||||
if (auth.$2 != null) {
|
||||
return auth.$2!;
|
||||
}
|
||||
final user = auth.$1!;
|
||||
if (!_isAdmin(user)) {
|
||||
return Response.forbidden(
|
||||
body: Body.fromString('Admin role required'),
|
||||
);
|
||||
}
|
||||
|
||||
final body = await req.asJson();
|
||||
final attachmentId = (body['attachmentId'] as String?)?.trim();
|
||||
if (attachmentId == null || attachmentId.isEmpty) {
|
||||
return Response.badRequest(
|
||||
body: Body.fromString('attachmentId is required'),
|
||||
);
|
||||
}
|
||||
|
||||
final query = fileBox.query(ChatFile_.attachmentId.equals(attachmentId)).build();
|
||||
final file = query.findFirst();
|
||||
query.close();
|
||||
if (file == null || file.status != 'ready') {
|
||||
return Response.notFound(
|
||||
body: Body.fromString('Attachment not found or not ready'),
|
||||
);
|
||||
}
|
||||
|
||||
file.usageType = usageType;
|
||||
fileBox.put(file);
|
||||
|
||||
final settings = _ensureServerSettings();
|
||||
if (target == 'icon') {
|
||||
settings.iconAttachmentId = attachmentId;
|
||||
} else {
|
||||
settings.bannerAttachmentId = attachmentId;
|
||||
}
|
||||
settings.updatedByUserId = user.userId;
|
||||
settings.updatedAt = DateTime.now();
|
||||
serverSettingsBox.put(settings);
|
||||
|
||||
return Response.ok(
|
||||
body: _serverSettingsToMap(settings).toBody,
|
||||
);
|
||||
}
|
||||
|
||||
(ChatUser?, Response?) _requireCurrentUser(Request req) {
|
||||
final userId = userIdProperty[req];
|
||||
if (userId == null) {
|
||||
return (null, Response.unauthorized(body: Body.fromString('Not authenticated')));
|
||||
}
|
||||
|
||||
final query = userBox.query(ChatUser_.userId.equals(userId)).build();
|
||||
final user = query.findFirst();
|
||||
query.close();
|
||||
if (user == null) {
|
||||
return (null, Response.unauthorized(body: Body.fromString('User not found')));
|
||||
}
|
||||
|
||||
return (user, null);
|
||||
}
|
||||
|
||||
bool _isAdmin(ChatUser user) => user.roles.contains('admin');
|
||||
|
||||
ServerSettings _ensureServerSettings() {
|
||||
final query = serverSettingsBox.query(ServerSettings_.singletonKey.equals('default')).build();
|
||||
final existing = query.findFirst();
|
||||
query.close();
|
||||
if (existing != null) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
final settings = ServerSettings(serverName: Config.serverName);
|
||||
settings.id = serverSettingsBox.put(settings);
|
||||
return settings;
|
||||
}
|
||||
|
||||
bool _matchesCurrentServer(String? serverId) {
|
||||
if (serverId == null || serverId.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
if (serverId == 'current') {
|
||||
return true;
|
||||
}
|
||||
return serverId == Config.publicHost;
|
||||
}
|
||||
|
||||
Map<String, dynamic> _serverSettingsToMap(ServerSettings settings) {
|
||||
return {
|
||||
'serverId': Config.publicHost,
|
||||
'publicBaseUrl': Config.publicBaseUrl,
|
||||
'serverName': settings.serverName,
|
||||
'description': settings.description,
|
||||
'iconUrl': settings.iconAttachmentId == null ? null : '/icons/${Uri.encodeComponent(Config.publicHost)}',
|
||||
'bannerUrl': settings.bannerAttachmentId == null ? null : '/banners/${Uri.encodeComponent(Config.publicHost)}',
|
||||
'iconAttachmentId': settings.iconAttachmentId,
|
||||
'bannerAttachmentId': settings.bannerAttachmentId,
|
||||
'updatedAt': settings.updatedAt.toIso8601String(),
|
||||
'updatedByUserId': settings.updatedByUserId,
|
||||
};
|
||||
}
|
||||
|
||||
Future<Response> _serveAttachment(
|
||||
String attachmentId, {
|
||||
required bool mutableAlias,
|
||||
}) async {
|
||||
final query = fileBox.query(ChatFile_.attachmentId.equals(attachmentId)).build();
|
||||
final file = query.findFirst();
|
||||
query.close();
|
||||
if (file == null || file.status != 'ready' || file.storedFileName.isEmpty) {
|
||||
return Response.notFound(
|
||||
body: Body.fromString('Attachment not found'),
|
||||
);
|
||||
}
|
||||
|
||||
final diskPath = 'uploads${Platform.pathSeparator}${file.storedFileName}';
|
||||
final diskFile = File(diskPath);
|
||||
if (!diskFile.existsSync()) {
|
||||
return Response.notFound(
|
||||
body: Body.fromString('Attachment file missing on disk'),
|
||||
);
|
||||
}
|
||||
|
||||
final cacheControl = mutableAlias ? 'public, max-age=0, must-revalidate' : 'public, max-age=31536000, immutable';
|
||||
|
||||
return Response.ok(
|
||||
headers: Headers.fromMap(
|
||||
{
|
||||
Headers.contentTypeHeader: [file.mimeType],
|
||||
Headers.cacheControlHeader: [cacheControl],
|
||||
},
|
||||
),
|
||||
body: Body.fromData(diskFile.readAsBytesSync()),
|
||||
);
|
||||
}
|
||||
@@ -1,529 +0,0 @@
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:chat_server/models.dart';
|
||||
import 'package:chat_server/objectbox.g.dart';
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:mime/mime.dart';
|
||||
import 'package:relic/relic.dart';
|
||||
|
||||
import '../auth/auth_middleware.dart';
|
||||
import '../state/app_state.dart';
|
||||
import '../utils/extensions.dart';
|
||||
import 'message_handlers.dart';
|
||||
|
||||
const _uploadsDir = 'uploads';
|
||||
const _maxUploadBytes = 50 * 1024 * 1024;
|
||||
|
||||
Future<Response> initUploadHandler(Request req) async {
|
||||
final currentUser = _requireCurrentUser(req);
|
||||
if (currentUser.$2 != null) {
|
||||
return currentUser.$2!;
|
||||
}
|
||||
final user = currentUser.$1!;
|
||||
|
||||
final body = await req.asJson();
|
||||
final usageType = (body['usageType'] as String?)?.trim() ?? 'message';
|
||||
final originalFileName = (body['originalFileName'] as String?)?.trim() ?? '';
|
||||
final declaredMimeType = (body['mimeType'] as String?)?.trim() ?? 'application/octet-stream';
|
||||
final declaredWidth = _asInt(body['width']) ?? 0;
|
||||
final declaredHeight = _asInt(body['height']) ?? 0;
|
||||
final declaredSizeBytes = _asInt(body['sizeBytes']) ?? 0;
|
||||
|
||||
if (!_isAllowedUsageType(usageType)) {
|
||||
return Response.badRequest(
|
||||
body: Body.fromString('Invalid usageType'),
|
||||
);
|
||||
}
|
||||
|
||||
final attachmentId = _generateAttachmentId();
|
||||
final entity = ChatFile(
|
||||
attachmentId: attachmentId,
|
||||
uploaderId: user.id,
|
||||
path: '',
|
||||
originalFileName: originalFileName.isEmpty ? 'upload-${attachmentId.substring(0, 8)}' : originalFileName,
|
||||
storedFileName: '',
|
||||
sha256: '',
|
||||
mimeType: declaredMimeType,
|
||||
width: declaredWidth,
|
||||
height: declaredHeight,
|
||||
sizeBytes: declaredSizeBytes,
|
||||
usageType: usageType,
|
||||
status: 'pending',
|
||||
);
|
||||
|
||||
final id = fileBox.put(entity);
|
||||
entity.id = id;
|
||||
|
||||
return Response.ok(
|
||||
body: {
|
||||
'uploadId': entity.attachmentId,
|
||||
'attachmentId': entity.attachmentId,
|
||||
'status': entity.status,
|
||||
'usageType': entity.usageType,
|
||||
}.toBody,
|
||||
);
|
||||
}
|
||||
|
||||
Future<Response> completeUploadHandler(Request req) async {
|
||||
final currentUser = _requireCurrentUser(req);
|
||||
if (currentUser.$2 != null) {
|
||||
return currentUser.$2!;
|
||||
}
|
||||
final user = currentUser.$1!;
|
||||
|
||||
final uploadId = req.rawPathParameters[#uploadId];
|
||||
if (uploadId == null || uploadId.isEmpty) {
|
||||
return Response.badRequest(
|
||||
body: Body.fromString('uploadId is required'),
|
||||
);
|
||||
}
|
||||
|
||||
final query = fileBox.query(ChatFile_.attachmentId.equals(uploadId)).build();
|
||||
final file = query.findFirst();
|
||||
query.close();
|
||||
|
||||
if (file == null) {
|
||||
return Response.notFound(
|
||||
body: Body.fromString('Upload not found'),
|
||||
);
|
||||
}
|
||||
|
||||
if (file.uploaderId != user.id) {
|
||||
return Response.forbidden(
|
||||
body: Body.fromString('You cannot complete another user\'s upload'),
|
||||
);
|
||||
}
|
||||
|
||||
if (file.status == 'ready') {
|
||||
return Response.ok(body: _fileResponseMap(file).toBody);
|
||||
}
|
||||
|
||||
final uploadData = await _readUploadData(req);
|
||||
if (uploadData.$2 != null) {
|
||||
file.status = 'failed';
|
||||
file.errorCode = 'invalid_upload_payload';
|
||||
fileBox.put(file);
|
||||
return uploadData.$2!;
|
||||
}
|
||||
final bytes = uploadData.$1!;
|
||||
final payloadMimeType = uploadData.$3;
|
||||
final payloadFileName = uploadData.$4;
|
||||
|
||||
if (bytes.isEmpty) {
|
||||
file.status = 'failed';
|
||||
file.errorCode = 'empty_file';
|
||||
fileBox.put(file);
|
||||
return Response.badRequest(
|
||||
body: Body.fromString('Uploaded file is empty'),
|
||||
);
|
||||
}
|
||||
|
||||
_finalizeUpload(
|
||||
file: file,
|
||||
bytes: bytes,
|
||||
payloadMimeType: payloadMimeType,
|
||||
payloadFileName: payloadFileName,
|
||||
);
|
||||
|
||||
await resolvePendingUploadsInMessages(file.attachmentId);
|
||||
|
||||
return Response.ok(
|
||||
body: _fileResponseMap(file).toBody,
|
||||
);
|
||||
}
|
||||
|
||||
Future<Response> uploadFileHandler(Request req) async {
|
||||
final currentUser = _requireCurrentUser(req);
|
||||
if (currentUser.$2 != null) {
|
||||
return currentUser.$2!;
|
||||
}
|
||||
final user = currentUser.$1!;
|
||||
|
||||
final uploadData = await _readUploadData(req);
|
||||
if (uploadData.$2 != null) {
|
||||
return uploadData.$2!;
|
||||
}
|
||||
final bytes = uploadData.$1!;
|
||||
final payloadMimeType = uploadData.$3;
|
||||
final payloadFileName = uploadData.$4;
|
||||
|
||||
final attachmentId = _generateAttachmentId();
|
||||
final file = ChatFile(
|
||||
attachmentId: attachmentId,
|
||||
uploaderId: user.id,
|
||||
path: '',
|
||||
originalFileName: payloadFileName ?? 'upload-${attachmentId.substring(0, 8)}',
|
||||
storedFileName: '',
|
||||
sha256: '',
|
||||
mimeType: _normalizeMimeType(payloadMimeType),
|
||||
width: 0,
|
||||
height: 0,
|
||||
sizeBytes: bytes.length,
|
||||
usageType: 'message',
|
||||
status: 'pending',
|
||||
);
|
||||
file.id = fileBox.put(file);
|
||||
|
||||
_finalizeUpload(
|
||||
file: file,
|
||||
bytes: bytes,
|
||||
payloadMimeType: payloadMimeType,
|
||||
payloadFileName: payloadFileName,
|
||||
);
|
||||
|
||||
return Response.ok(
|
||||
body: _fileResponseMap(file).toBody,
|
||||
);
|
||||
}
|
||||
|
||||
Future<Response> getAttachmentHandler(Request req) async {
|
||||
final attachmentId = req.rawPathParameters[#attachmentId];
|
||||
if (attachmentId == null || attachmentId.isEmpty) {
|
||||
return Response.badRequest(
|
||||
body: Body.fromString('Invalid attachment ID'),
|
||||
);
|
||||
}
|
||||
|
||||
final query = fileBox.query(ChatFile_.attachmentId.equals(attachmentId)).build();
|
||||
final file = query.findFirst();
|
||||
query.close();
|
||||
|
||||
if (file == null || file.status != 'ready') {
|
||||
return Response.notFound(
|
||||
body: Body.fromString('Attachment not found'),
|
||||
);
|
||||
}
|
||||
|
||||
final diskPath = _resolveDiskPath(file);
|
||||
final diskFile = File(diskPath);
|
||||
if (!diskFile.existsSync()) {
|
||||
return Response.notFound(
|
||||
body: Body.fromString('Attachment file missing on disk'),
|
||||
);
|
||||
}
|
||||
|
||||
return Response.ok(
|
||||
headers: Headers.fromMap(
|
||||
{
|
||||
Headers.contentTypeHeader: [file.mimeType],
|
||||
Headers.cacheControlHeader: ['public, max-age=31536000, immutable'],
|
||||
},
|
||||
),
|
||||
body: Body.fromData(diskFile.readAsBytesSync()),
|
||||
);
|
||||
}
|
||||
|
||||
Future<Response> getUploadFileHandler(Request req) async {
|
||||
final fileName = req.rawPathParameters[#fileName];
|
||||
if (fileName == null || fileName.isEmpty || fileName.contains('..')) {
|
||||
return Response.badRequest(
|
||||
body: Body.fromString('Invalid file name'),
|
||||
);
|
||||
}
|
||||
|
||||
final diskPath = '$_uploadsDir${Platform.pathSeparator}$fileName';
|
||||
final file = File(diskPath);
|
||||
if (!file.existsSync()) {
|
||||
return Response.notFound(
|
||||
body: Body.fromString('File not found'),
|
||||
);
|
||||
}
|
||||
|
||||
return Response.ok(
|
||||
body: Body.fromData(file.readAsBytesSync()),
|
||||
);
|
||||
}
|
||||
|
||||
(ChatUser?, Response?) _requireCurrentUser(Request req) {
|
||||
final userId = userIdProperty[req];
|
||||
if (userId == null) {
|
||||
return (
|
||||
null,
|
||||
Response.unauthorized(
|
||||
body: Body.fromString('Not authenticated'),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
final query = userBox.query(ChatUser_.userId.equals(userId)).build();
|
||||
final user = query.findFirst();
|
||||
query.close();
|
||||
if (user == null) {
|
||||
return (
|
||||
null,
|
||||
Response.unauthorized(
|
||||
body: Body.fromString('User not found'),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return (user, null);
|
||||
}
|
||||
|
||||
Future<(Uint8List?, Response?, String?, String?)> _readUploadData(Request req) async {
|
||||
final contentTypeHeader = req.headers[Headers.contentTypeHeader]?.first;
|
||||
final boundary = _extractBoundary(contentTypeHeader);
|
||||
|
||||
if (boundary != null) {
|
||||
final multipartParts =
|
||||
await req.read(maxLength: _maxUploadBytes).cast<List<int>>().transform(MimeMultipartTransformer(boundary)).toList();
|
||||
if (multipartParts.isEmpty) {
|
||||
return (null, Response.badRequest(body: Body.fromString('No files in request')), null, null);
|
||||
}
|
||||
|
||||
final uploadPart = multipartParts.firstWhere(
|
||||
(part) => (part.headers['content-disposition'] ?? '').contains('filename='),
|
||||
orElse: () => multipartParts.first,
|
||||
);
|
||||
|
||||
final bytesBuilder = BytesBuilder(copy: false);
|
||||
await for (final chunk in uploadPart) {
|
||||
bytesBuilder.add(chunk);
|
||||
}
|
||||
final bytes = bytesBuilder.toBytes();
|
||||
final mimeType = uploadPart.headers['content-type'];
|
||||
final filename = _extractFilename(uploadPart.headers['content-disposition'] ?? '');
|
||||
|
||||
return (bytes, null, mimeType, filename);
|
||||
}
|
||||
|
||||
final bytesBuilder = BytesBuilder(copy: false);
|
||||
await for (final chunk in req.read(maxLength: _maxUploadBytes)) {
|
||||
bytesBuilder.add(chunk);
|
||||
}
|
||||
|
||||
final mimeType = contentTypeHeader;
|
||||
return (bytesBuilder.toBytes(), null, mimeType, null);
|
||||
}
|
||||
|
||||
Map<String, dynamic> _fileResponseMap(ChatFile file) {
|
||||
return {
|
||||
'uploadId': file.attachmentId,
|
||||
'attachmentId': file.attachmentId,
|
||||
'id': file.id,
|
||||
'url': file.path,
|
||||
'status': file.status,
|
||||
'usageType': file.usageType,
|
||||
'originalFileName': file.originalFileName,
|
||||
'storedFileName': file.storedFileName,
|
||||
'mimeType': file.mimeType,
|
||||
'sizeBytes': file.sizeBytes,
|
||||
'width': file.width,
|
||||
'height': file.height,
|
||||
'sha256': file.sha256,
|
||||
};
|
||||
}
|
||||
|
||||
void _finalizeUpload({
|
||||
required ChatFile file,
|
||||
required Uint8List bytes,
|
||||
String? payloadMimeType,
|
||||
String? payloadFileName,
|
||||
}) {
|
||||
final detectedMime = _normalizeMimeType(
|
||||
payloadMimeType ?? file.mimeType,
|
||||
);
|
||||
final dimensions = _extractImageDimensions(bytes, detectedMime);
|
||||
final extension = _resolveExtension(
|
||||
payloadFileName ?? file.originalFileName,
|
||||
detectedMime,
|
||||
);
|
||||
final storedFileName = '${file.attachmentId}$extension';
|
||||
|
||||
final directory = Directory(_uploadsDir);
|
||||
if (!directory.existsSync()) {
|
||||
directory.createSync(recursive: true);
|
||||
}
|
||||
|
||||
final diskPath = '${directory.path}${Platform.pathSeparator}$storedFileName';
|
||||
File(diskPath).writeAsBytesSync(bytes, flush: true);
|
||||
|
||||
file.path = '/attachments/${file.attachmentId}';
|
||||
file.storedFileName = storedFileName;
|
||||
file.sha256 = sha256.convert(bytes).toString();
|
||||
file.mimeType = detectedMime;
|
||||
file.width = dimensions.$1;
|
||||
file.height = dimensions.$2;
|
||||
file.sizeBytes = bytes.length;
|
||||
file.status = 'ready';
|
||||
file.errorCode = null;
|
||||
fileBox.put(file);
|
||||
}
|
||||
|
||||
String _generateAttachmentId() {
|
||||
const alphabet = '0123456789abcdefghijklmnopqrstuvwxyz';
|
||||
final random = Random.secure();
|
||||
String next() {
|
||||
final bytes = List<int>.generate(20, (_) => random.nextInt(256));
|
||||
final sb = StringBuffer();
|
||||
for (final byte in bytes) {
|
||||
sb.write(alphabet[byte % alphabet.length]);
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
while (true) {
|
||||
final id = next();
|
||||
final exists = fileBox.query(ChatFile_.attachmentId.equals(id)).build().findFirst();
|
||||
if (exists == null) {
|
||||
return id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool _isAllowedUsageType(String usageType) {
|
||||
return usageType == 'message' || usageType == 'avatar' || usageType == 'server_icon' || usageType == 'server_banner';
|
||||
}
|
||||
|
||||
int? _asInt(Object? value) {
|
||||
return switch (value) {
|
||||
int v => v,
|
||||
double v => v.round(),
|
||||
String v => int.tryParse(v),
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
String _resolveDiskPath(ChatFile file) {
|
||||
final storedFileName = file.storedFileName.trim();
|
||||
if (storedFileName.isNotEmpty) {
|
||||
return '$_uploadsDir${Platform.pathSeparator}$storedFileName';
|
||||
}
|
||||
|
||||
final path = file.path;
|
||||
if (path.startsWith('/uploads/')) {
|
||||
final fileName = path.substring('/uploads/'.length);
|
||||
return '$_uploadsDir${Platform.pathSeparator}$fileName';
|
||||
}
|
||||
|
||||
final attachmentPath = path.startsWith('/attachments/') ? path.substring('/attachments/'.length) : path;
|
||||
return '$_uploadsDir${Platform.pathSeparator}$attachmentPath';
|
||||
}
|
||||
|
||||
String _normalizeMimeType(String? mimeType) {
|
||||
final cleaned = (mimeType ?? '').split(';').first.trim().toLowerCase();
|
||||
if (cleaned.isEmpty) {
|
||||
return 'application/octet-stream';
|
||||
}
|
||||
return cleaned;
|
||||
}
|
||||
|
||||
String? _extractBoundary(String? contentTypeHeader) {
|
||||
if (contentTypeHeader == null) {
|
||||
return null;
|
||||
}
|
||||
if (!contentTypeHeader.toLowerCase().startsWith('multipart/form-data')) {
|
||||
return null;
|
||||
}
|
||||
final match = RegExp(
|
||||
r'boundary="?([^\";]+)"?',
|
||||
caseSensitive: false,
|
||||
).firstMatch(contentTypeHeader);
|
||||
return match?.group(1);
|
||||
}
|
||||
|
||||
String? _extractFilename(String contentDisposition) {
|
||||
final match = RegExp(
|
||||
r'filename="([^"]+)"',
|
||||
caseSensitive: false,
|
||||
).firstMatch(contentDisposition);
|
||||
if (match == null) {
|
||||
return null;
|
||||
}
|
||||
final decoded = Uri.decodeFull(match.group(1)!);
|
||||
return decoded.split(RegExp(r'[\\/]')).last;
|
||||
}
|
||||
|
||||
String _resolveExtension(String? fileName, String mimeType) {
|
||||
final lowerName = fileName?.toLowerCase();
|
||||
if (lowerName != null) {
|
||||
final dotIndex = lowerName.lastIndexOf('.');
|
||||
if (dotIndex > 0 && dotIndex < lowerName.length - 1) {
|
||||
return lowerName.substring(dotIndex);
|
||||
}
|
||||
}
|
||||
|
||||
return switch (mimeType.toLowerCase()) {
|
||||
'image/png' => '.png',
|
||||
'image/jpeg' || 'image/jpg' => '.jpg',
|
||||
'image/gif' => '.gif',
|
||||
'image/webp' => '.webp',
|
||||
'image/avif' => '.avif',
|
||||
'video/mp4' => '.mp4',
|
||||
_ => '.bin',
|
||||
};
|
||||
}
|
||||
|
||||
(int, int) _extractImageDimensions(Uint8List bytes, String mimeType) {
|
||||
final lowerMime = mimeType.toLowerCase();
|
||||
|
||||
if (lowerMime == 'image/png' && bytes.length >= 24) {
|
||||
final width = _readUint32BigEndian(bytes, 16);
|
||||
final height = _readUint32BigEndian(bytes, 20);
|
||||
return (width, height);
|
||||
}
|
||||
|
||||
if ((lowerMime == 'image/jpeg' || lowerMime == 'image/jpg') && bytes.length > 4) {
|
||||
final jpeg = _extractJpegDimensions(bytes);
|
||||
if (jpeg != null) {
|
||||
return jpeg;
|
||||
}
|
||||
}
|
||||
|
||||
if (lowerMime == 'image/gif' && bytes.length >= 10) {
|
||||
final width = bytes[6] | (bytes[7] << 8);
|
||||
final height = bytes[8] | (bytes[9] << 8);
|
||||
return (width, height);
|
||||
}
|
||||
|
||||
return (0, 0);
|
||||
}
|
||||
|
||||
int _readUint32BigEndian(Uint8List bytes, int offset) {
|
||||
return (bytes[offset] << 24) | (bytes[offset + 1] << 16) | (bytes[offset + 2] << 8) | bytes[offset + 3];
|
||||
}
|
||||
|
||||
(int, int)? _extractJpegDimensions(Uint8List bytes) {
|
||||
if (bytes[0] != 0xFF || bytes[1] != 0xD8) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var offset = 2;
|
||||
while (offset + 9 < bytes.length) {
|
||||
if (bytes[offset] != 0xFF) {
|
||||
offset++;
|
||||
continue;
|
||||
}
|
||||
|
||||
final marker = bytes[offset + 1];
|
||||
final hasSize = marker != 0xD8 && marker != 0xD9;
|
||||
if (!hasSize) {
|
||||
offset += 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (offset + 4 >= bytes.length) {
|
||||
return null;
|
||||
}
|
||||
final length = (bytes[offset + 2] << 8) | bytes[offset + 3];
|
||||
if (length < 2 || offset + 2 + length > bytes.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final isSofMarker = (marker >= 0xC0 && marker <= 0xC3) ||
|
||||
(marker >= 0xC5 && marker <= 0xC7) ||
|
||||
(marker >= 0xC9 && marker <= 0xCB) ||
|
||||
(marker >= 0xCD && marker <= 0xCF);
|
||||
if (isSofMarker && offset + 9 < bytes.length) {
|
||||
final height = (bytes[offset + 5] << 8) | bytes[offset + 6];
|
||||
final width = (bytes[offset + 7] << 8) | bytes[offset + 8];
|
||||
return (width, height);
|
||||
}
|
||||
|
||||
offset += 2 + length;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import 'package:chat_server/objectbox.g.dart';
|
||||
import 'package:relic/relic.dart';
|
||||
|
||||
import '../utils/extensions.dart';
|
||||
|
||||
Future<Response> getUsersHandler(Request req) async {
|
||||
final users = userBox.getAll();
|
||||
|
||||
return Response.ok(
|
||||
body: {
|
||||
'users': users.map((u) => {'id': u.id, 'username': u.username, 'createdAt': u.createdAt.toIso8601String()}).toList(),
|
||||
}.toBody,
|
||||
);
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import 'package:chat_server/models.dart';
|
||||
import 'package:chat_server/objectbox.g.dart';
|
||||
|
||||
late final Store store;
|
||||
late final Box<ChatUser> userBox;
|
||||
late final Box<ChatChannel> channelBox;
|
||||
late final Box<ChatMessage> messageBox;
|
||||
late final Box<ChatMessageMedia> messageMediaBox;
|
||||
late final Box<ChatFile> fileBox;
|
||||
late final Box<ServerSettings> serverSettingsBox;
|
||||
|
||||
void initializeStore() {
|
||||
store = openStore();
|
||||
userBox = store.box<ChatUser>();
|
||||
channelBox = store.box<ChatChannel>();
|
||||
messageBox = store.box<ChatMessage>();
|
||||
messageMediaBox = store.box<ChatMessageMedia>();
|
||||
fileBox = store.box<ChatFile>();
|
||||
serverSettingsBox = store.box<ServerSettings>();
|
||||
}
|
||||
@@ -2,6 +2,8 @@ import 'dart:convert';
|
||||
|
||||
import 'package:relic/relic.dart';
|
||||
|
||||
import '../config/config.dart';
|
||||
|
||||
extension RequestHelper on Request {
|
||||
/// Parses the request body as JSON
|
||||
Future<Map<String, dynamic>> asJson() async {
|
||||
|
||||
Executable
Whitespace-only changes.
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
import "./.next/dev/types/routes.d.ts";
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
Executable
+12
@@ -0,0 +1,12 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
output: "standalone",
|
||||
/* config options here */
|
||||
typescript: {
|
||||
ignoreBuildErrors: true,
|
||||
},
|
||||
reactStrictMode: false,
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Executable
+94
@@ -0,0 +1,94 @@
|
||||
{
|
||||
"name": "nextjs_tailwind_shadcn_ts",
|
||||
"version": "0.2.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev -p 3000 2>&1 | tee dev.log",
|
||||
"build": "next build && cp -r .next/static .next/standalone/.next/ && cp -r public .next/standalone/",
|
||||
"start": "NODE_ENV=production bun .next/standalone/server.js 2>&1 | tee server.log",
|
||||
"lint": "eslint .",
|
||||
"db:push": "prisma db push",
|
||||
"db:generate": "prisma generate",
|
||||
"db:migrate": "prisma migrate dev",
|
||||
"db:reset": "prisma migrate reset"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@hookform/resolvers": "^5.1.1",
|
||||
"@mdxeditor/editor": "^3.39.1",
|
||||
"@prisma/client": "^6.11.1",
|
||||
"@radix-ui/react-accordion": "^1.2.11",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.14",
|
||||
"@radix-ui/react-aspect-ratio": "^1.1.7",
|
||||
"@radix-ui/react-avatar": "^1.1.10",
|
||||
"@radix-ui/react-checkbox": "^1.3.2",
|
||||
"@radix-ui/react-collapsible": "^1.1.11",
|
||||
"@radix-ui/react-context-menu": "^2.2.15",
|
||||
"@radix-ui/react-dialog": "^1.1.14",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||
"@radix-ui/react-hover-card": "^1.1.14",
|
||||
"@radix-ui/react-label": "^2.1.7",
|
||||
"@radix-ui/react-menubar": "^1.1.15",
|
||||
"@radix-ui/react-navigation-menu": "^1.2.13",
|
||||
"@radix-ui/react-popover": "^1.1.14",
|
||||
"@radix-ui/react-progress": "^1.1.7",
|
||||
"@radix-ui/react-radio-group": "^1.3.7",
|
||||
"@radix-ui/react-scroll-area": "^1.2.9",
|
||||
"@radix-ui/react-select": "^2.2.5",
|
||||
"@radix-ui/react-separator": "^1.1.7",
|
||||
"@radix-ui/react-slider": "^1.3.5",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-switch": "^1.2.5",
|
||||
"@radix-ui/react-tabs": "^1.1.12",
|
||||
"@radix-ui/react-toast": "^1.2.14",
|
||||
"@radix-ui/react-toggle": "^1.1.9",
|
||||
"@radix-ui/react-toggle-group": "^1.1.10",
|
||||
"@radix-ui/react-tooltip": "^1.2.7",
|
||||
"@reactuses/core": "^6.0.5",
|
||||
"@tanstack/react-query": "^5.82.0",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"framer-motion": "^12.23.2",
|
||||
"input-otp": "^1.4.2",
|
||||
"lucide-react": "^0.525.0",
|
||||
"next": "^16.1.1",
|
||||
"next-auth": "^4.24.11",
|
||||
"next-intl": "^4.3.4",
|
||||
"next-themes": "^0.4.6",
|
||||
"prisma": "^6.11.1",
|
||||
"react": "^19.0.0",
|
||||
"react-day-picker": "^9.8.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-hook-form": "^7.60.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-resizable-panels": "^3.0.3",
|
||||
"react-syntax-highlighter": "^15.6.1",
|
||||
"recharts": "^2.15.4",
|
||||
"sharp": "^0.34.3",
|
||||
"sonner": "^2.0.6",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"uuid": "^11.1.0",
|
||||
"vaul": "^1.1.2",
|
||||
"z-ai-web-dev-sdk": "^0.0.16",
|
||||
"zod": "^4.0.2",
|
||||
"zustand": "^5.0.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"bun-types": "^1.3.4",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "^16.1.1",
|
||||
"tailwindcss": "^4",
|
||||
"tw-animate-css": "^1.3.5",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
Executable
+5
@@ -0,0 +1,5 @@
|
||||
const config = {
|
||||
plugins: ["@tailwindcss/postcss"],
|
||||
};
|
||||
|
||||
export default config;
|
||||
Executable
+32
@@ -0,0 +1,32 @@
|
||||
// This is your Prisma schema file,
|
||||
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
||||
|
||||
// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
|
||||
// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "sqlite"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id @default(cuid())
|
||||
email String @unique
|
||||
name String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model Post {
|
||||
id String @id @default(cuid())
|
||||
title String
|
||||
content String?
|
||||
published Boolean @default(false)
|
||||
authorId String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
Executable
+29
@@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 30 30" style="enable-background:new 0 0 30 30;" xml:space="preserve">
|
||||
<defs>
|
||||
<style type="text/css">
|
||||
.st194{fill:#2D2D2D;stroke:#FFFFFF;stroke-width:0.6317;stroke-miterlimit:10;}
|
||||
.st23{fill:#FFFFFF;}
|
||||
|
||||
.z-breathe {
|
||||
animation: breathe 2.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes breathe {
|
||||
0%, 100% { opacity: 0.7; }
|
||||
50% { opacity: 1; }
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
|
||||
<g>
|
||||
<path class="st194" d="M24.51,28.51H5.49c-2.21,0-4-1.79-4-4V5.49c0-2.21,1.79-4,4-4h19.03c2.21,0,4,1.79,4,4v19.03
|
||||
C28.51,26.72,26.72,28.51,24.51,28.51z"/>
|
||||
<g class="z-breathe">
|
||||
<path class="st23" d="M15.47,7.1l-1.3,1.85c-0.2,0.29-0.54,0.47-0.9,0.47h-7.1V7.09C6.16,7.1,15.47,7.1,15.47,7.1z"/>
|
||||
<polygon class="st23" points="24.3,7.1 13.14,22.91 5.7,22.91 16.86,7.1"/>
|
||||
<path class="st23" d="M14.53,22.91l1.31-1.86c0.2-0.29,0.54-0.47,0.9-0.47h7.09v2.33H14.53z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
Executable
+14
@@ -0,0 +1,14 @@
|
||||
User-agent: Googlebot
|
||||
Allow: /
|
||||
|
||||
User-agent: Bingbot
|
||||
Allow: /
|
||||
|
||||
User-agent: Twitterbot
|
||||
Allow: /
|
||||
|
||||
User-agent: facebookexternalhit
|
||||
Allow: /
|
||||
|
||||
User-agent: *
|
||||
Allow: /
|
||||
+57
-90
@@ -5,10 +5,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: _fe_analyzer_shared
|
||||
sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7"
|
||||
sha256: "796d97d925add7ffcdf5595f33a2066a6e3cee97971e6dbef09b76b7880fd760"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "67.0.0"
|
||||
version: "94.0.0"
|
||||
adaptive_number:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -21,10 +21,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: analyzer
|
||||
sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d"
|
||||
sha256: "9c8ebb304d72c0a0c8764344627529d9503fc83d7d73e43ed727dc532f822e4b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.4.1"
|
||||
version: "10.0.2"
|
||||
args:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -33,14 +33,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.7.0"
|
||||
asn1lib:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: asn1lib
|
||||
sha256: "9a8f69025044eb466b9b60ef3bc3ac99b4dc6c158ae9c56d25eeccf5bc56d024"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.6.5"
|
||||
async:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -69,18 +61,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build
|
||||
sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0"
|
||||
sha256: "275bf6bb2a00a9852c28d4e0b410da1d833a734d57d39d44f94bfc895a484ec3"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.1"
|
||||
version: "4.0.4"
|
||||
build_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_config
|
||||
sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33"
|
||||
sha256: "4f64382b97504dc2fcdf487d5aae33418e08b4703fc21249e4db6d804a4d0187"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.2"
|
||||
version: "1.2.0"
|
||||
build_daemon:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -89,30 +81,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.1"
|
||||
build_resolvers:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_resolvers
|
||||
sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.2"
|
||||
build_runner:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: build_runner
|
||||
sha256: "028819cfb90051c6b5440c7e574d1896f8037e3c96cf17aaeb054c9311cfbf4d"
|
||||
sha256: "39ad4ca8a2876779737c60e4228b4bcd35d4352ef7e14e47514093edc012c734"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.13"
|
||||
build_runner_core:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_runner_core
|
||||
sha256: f8126682b87a7282a339b871298cc12009cb67109cfa1614d6436fb0289193e0
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.3.2"
|
||||
version: "2.11.1"
|
||||
built_collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -162,7 +138,7 @@ packages:
|
||||
source: hosted
|
||||
version: "4.11.1"
|
||||
collection:
|
||||
dependency: "direct main"
|
||||
dependency: transitive
|
||||
description:
|
||||
name: collection
|
||||
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
|
||||
@@ -186,37 +162,29 @@ packages:
|
||||
source: hosted
|
||||
version: "1.15.0"
|
||||
crypto:
|
||||
dependency: "direct main"
|
||||
dependency: transitive
|
||||
description:
|
||||
name: crypto
|
||||
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.7"
|
||||
crypton:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: crypton
|
||||
sha256: "17b6631fbf89e389d421b46629132287ed37d601b2ad1357445826ab85022271"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.1"
|
||||
dart_jsonwebtoken:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: dart_jsonwebtoken
|
||||
sha256: b268ff9bb87a16360a397f1accb50e7c781384e1119010b34f6d7c25820c44d8
|
||||
sha256: "0de65691c1d736e9459f22f654ddd6fd8368a271d4e41aa07e53e6301eff5075"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.1"
|
||||
version: "3.3.1"
|
||||
dart_style:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dart_style
|
||||
sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9"
|
||||
sha256: "15a7db352c8fc6a4d2bc475ba901c25b39fe7157541da4c16eacce6f8be83e49"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.6"
|
||||
version: "3.1.5"
|
||||
ed25519_edwards:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -253,10 +221,17 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flat_buffers
|
||||
sha256: "380bdcba5664a718bfd4ea20a45d39e13684f5318fcd8883066a55e21f37f4c3"
|
||||
sha256: "7c1de2d6eb5f3e61e5c50040841109f509deaaf2b12ec0d57b92456d9ea50345"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "23.5.26"
|
||||
version: "25.9.23"
|
||||
fox_utils2:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "../fox_utils2"
|
||||
relative: true
|
||||
source: path
|
||||
version: "1.0.0"
|
||||
frontend_server_client:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -281,8 +256,16 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.2"
|
||||
hotreloader:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: hotreloader
|
||||
sha256: bc167a1163807b03bada490bfe2df25b0d744df359227880220a5cbd04e5734b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.3.0"
|
||||
http:
|
||||
dependency: transitive
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: http
|
||||
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
|
||||
@@ -313,22 +296,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.5"
|
||||
js:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: js
|
||||
sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.2"
|
||||
json_annotation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: json_annotation
|
||||
sha256: cb09e7dac6210041fad964ed7fbee004f14258b4eca4040f72d1234062ace4c8
|
||||
sha256: "805fa86df56383000f640384b282ce0cb8431f1a7a2396de92fb66186d8c57df"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.11.0"
|
||||
version: "4.10.0"
|
||||
lints:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
@@ -349,10 +324,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: matcher
|
||||
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
|
||||
sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.12.17"
|
||||
version: "0.12.18"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -362,7 +337,7 @@ packages:
|
||||
source: hosted
|
||||
version: "1.18.1"
|
||||
mime:
|
||||
dependency: "direct main"
|
||||
dependency: transitive
|
||||
description:
|
||||
name: mime
|
||||
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
|
||||
@@ -381,18 +356,18 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: objectbox
|
||||
sha256: "3d1cb5f9aa564f95c76ba251299f6cb1591c3dd8ff05fd76fa0549d899d9fe31"
|
||||
sha256: ac1e1ef852f60ea0af01fddb28811c8aa169f2b1ae9284e91e7b822abe5ed603
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.0"
|
||||
version: "5.2.0"
|
||||
objectbox_generator:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: objectbox_generator
|
||||
sha256: "777924e14daf18603a023b346333acac1c4fce9fe4a715b85d98cfcc723d9a1a"
|
||||
sha256: "28628fa65d4fe8ade868cd50aa323ecdfd6d435fcda2cc7484071b84031f8c5e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.0"
|
||||
version: "5.2.0"
|
||||
package_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -413,10 +388,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pointycastle
|
||||
sha256: "4be0097fcf3fd3e8449e53730c631200ebc7b88016acecab2b0da2f0149222fe"
|
||||
sha256: "92aa3841d083cc4b0f4709b5c74fd6409a3e6ba833ffc7dc6a8fee096366acf5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.9.1"
|
||||
version: "4.0.0"
|
||||
pool:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -493,18 +468,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shelf_web_socket
|
||||
sha256: cc36c297b52866d203dbf9332263c94becc2fe0ceaa9681d07b6ef9807023b67
|
||||
sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
version: "3.0.0"
|
||||
source_gen:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_gen
|
||||
sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832"
|
||||
sha256: "1d562a3c1f713904ebbed50d2760217fd8a51ca170ac4b05b0db490699dbac17"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.5.0"
|
||||
version: "4.2.0"
|
||||
source_map_stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -573,34 +548,26 @@ packages:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: test
|
||||
sha256: "75906bf273541b676716d1ca7627a17e4c4070a3a16272b7a3dc7da3b9f3f6b7"
|
||||
sha256: "54c516bbb7cee2754d327ad4fca637f78abfc3cbcc5ace83b3eda117e42cd71a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.26.3"
|
||||
version: "1.29.0"
|
||||
test_api:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
|
||||
sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.7"
|
||||
version: "0.7.9"
|
||||
test_core:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_core
|
||||
sha256: "0cc24b5ff94b38d2ae73e1eb43cc302b77964fbf67abad1e296025b78deb53d0"
|
||||
sha256: "394f07d21f0f2255ec9e3989f21e54d3c7dc0e6e9dbce160e5a9c1a6be0e2943"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.6.12"
|
||||
timing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: timing
|
||||
sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
version: "0.6.15"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -658,7 +625,7 @@ packages:
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
yaml:
|
||||
dependency: "direct main"
|
||||
dependency: transitive
|
||||
description:
|
||||
name: yaml
|
||||
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
|
||||
@@ -666,4 +633,4 @@ packages:
|
||||
source: hosted
|
||||
version: "3.1.3"
|
||||
sdks:
|
||||
dart: ">=3.9.0 <4.0.0"
|
||||
dart: ">=3.11.0-296.5.beta <4.0.0"
|
||||
+12
-14
@@ -3,21 +3,19 @@ description: A self-hosted chat server with home server identity model.
|
||||
version: 0.1.0
|
||||
|
||||
environment:
|
||||
sdk: ">=3.0.0 <4.0.0"
|
||||
sdk: '>=3.0.0 <4.0.0'
|
||||
|
||||
dependencies:
|
||||
relic:
|
||||
objectbox:
|
||||
dart_jsonwebtoken:
|
||||
bcrypt:
|
||||
crypton:
|
||||
crypto:
|
||||
collection:
|
||||
yaml:
|
||||
mime:
|
||||
relic: ^1.0.0
|
||||
objectbox: ^4.0.0
|
||||
dart_jsonwebtoken: ^2.14.0
|
||||
bcrypt: ^1.1.3
|
||||
crypton: ^2.2.1
|
||||
crypto: ^3.0.3
|
||||
collection: ^1.18.0
|
||||
hotreloader: ^1.0.6
|
||||
|
||||
dev_dependencies:
|
||||
test:
|
||||
objectbox_generator:
|
||||
build_runner:
|
||||
lints:
|
||||
objectbox_generator: ^4.0.0
|
||||
build_runner: ^2.4.0
|
||||
lints: ^4.0.0
|
||||
Executable
+21
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 z-ai-web-dev-sdk Skills
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
Executable
+580
@@ -0,0 +1,580 @@
|
||||
---
|
||||
name: ASR
|
||||
description: Implement speech-to-text (ASR/automatic speech recognition) capabilities using the z-ai-web-dev-sdk. Use this skill when the user needs to transcribe audio files, convert speech to text, build voice input features, or process audio recordings. Supports base64 encoded audio files and returns accurate text transcriptions.
|
||||
license: MIT
|
||||
---
|
||||
|
||||
# ASR (Speech to Text) Skill
|
||||
|
||||
This skill guides the implementation of speech-to-text (ASR) functionality using the z-ai-web-dev-sdk package, enabling accurate transcription of spoken audio into text.
|
||||
|
||||
## Skills Path
|
||||
|
||||
**Skill Location**: `{project_path}/skills/ASR`
|
||||
|
||||
this skill is located at above path in your project.
|
||||
|
||||
**Reference Scripts**: Example test scripts are available in the `{Skill Location}/scripts/` directory for quick testing and reference. See `{Skill Location}/scripts/asr.ts` for a working example.
|
||||
|
||||
## Overview
|
||||
|
||||
Speech-to-Text (ASR - Automatic Speech Recognition) allows you to build applications that convert spoken language in audio files into written text, enabling voice-controlled interfaces, transcription services, and audio content analysis.
|
||||
|
||||
**IMPORTANT**: z-ai-web-dev-sdk MUST be used in backend code only. Never use it in client-side code.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
The z-ai-web-dev-sdk package is already installed. Import it as shown in the examples below.
|
||||
|
||||
## CLI Usage (For Simple Tasks)
|
||||
|
||||
For simple audio transcription tasks, you can use the z-ai CLI instead of writing code. This is ideal for quick transcriptions, testing audio files, or batch processing.
|
||||
|
||||
### Basic Transcription from File
|
||||
|
||||
```bash
|
||||
# Transcribe an audio file
|
||||
z-ai asr --file ./audio.wav
|
||||
|
||||
# Save transcription to JSON file
|
||||
z-ai asr -f ./recording.mp3 -o transcript.json
|
||||
|
||||
# Transcribe and view output
|
||||
z-ai asr --file ./interview.wav --output result.json
|
||||
```
|
||||
|
||||
### Transcription from Base64
|
||||
|
||||
```bash
|
||||
# Transcribe from base64 encoded audio
|
||||
z-ai asr --base64 "UklGRiQAAABXQVZFZm10..." -o result.json
|
||||
|
||||
# Using short option
|
||||
z-ai asr -b "base64_encoded_audio_data" -o transcript.json
|
||||
```
|
||||
|
||||
### Streaming Output
|
||||
|
||||
```bash
|
||||
# Stream transcription results
|
||||
z-ai asr -f ./audio.wav --stream
|
||||
```
|
||||
|
||||
### CLI Parameters
|
||||
|
||||
- `--file, -f <path>`: **Required** (if not using --base64) - Audio file path
|
||||
- `--base64, -b <base64>`: **Required** (if not using --file) - Base64 encoded audio
|
||||
- `--output, -o <path>`: Optional - Output file path (JSON format)
|
||||
- `--stream`: Optional - Stream the transcription output
|
||||
|
||||
### Supported Audio Formats
|
||||
|
||||
The ASR service supports various audio formats including:
|
||||
- WAV (.wav)
|
||||
- MP3 (.mp3)
|
||||
- Other common audio formats
|
||||
|
||||
### When to Use CLI vs SDK
|
||||
|
||||
**Use CLI for:**
|
||||
- Quick audio file transcriptions
|
||||
- Testing audio recognition accuracy
|
||||
- Simple batch processing scripts
|
||||
- One-off transcription tasks
|
||||
|
||||
**Use SDK for:**
|
||||
- Real-time audio transcription in applications
|
||||
- Integration with recording systems
|
||||
- Custom audio processing workflows
|
||||
- Production applications with streaming audio
|
||||
|
||||
## Basic ASR Implementation
|
||||
|
||||
### Simple Audio Transcription
|
||||
|
||||
```javascript
|
||||
import ZAI from 'z-ai-web-dev-sdk';
|
||||
import fs from 'fs';
|
||||
|
||||
async function transcribeAudio(audioFilePath) {
|
||||
const zai = await ZAI.create();
|
||||
|
||||
// Read audio file and convert to base64
|
||||
const audioFile = fs.readFileSync(audioFilePath);
|
||||
const base64Audio = audioFile.toString('base64');
|
||||
|
||||
const response = await zai.audio.asr.create({
|
||||
file_base64: base64Audio
|
||||
});
|
||||
|
||||
return response.text;
|
||||
}
|
||||
|
||||
// Usage
|
||||
const transcription = await transcribeAudio('./audio.wav');
|
||||
console.log('Transcription:', transcription);
|
||||
```
|
||||
|
||||
### Transcribe Multiple Audio Files
|
||||
|
||||
```javascript
|
||||
import ZAI from 'z-ai-web-dev-sdk';
|
||||
import fs from 'fs';
|
||||
|
||||
async function transcribeBatch(audioFilePaths) {
|
||||
const zai = await ZAI.create();
|
||||
const results = [];
|
||||
|
||||
for (const filePath of audioFilePaths) {
|
||||
try {
|
||||
const audioFile = fs.readFileSync(filePath);
|
||||
const base64Audio = audioFile.toString('base64');
|
||||
|
||||
const response = await zai.audio.asr.create({
|
||||
file_base64: base64Audio
|
||||
});
|
||||
|
||||
results.push({
|
||||
file: filePath,
|
||||
success: true,
|
||||
transcription: response.text
|
||||
});
|
||||
} catch (error) {
|
||||
results.push({
|
||||
file: filePath,
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
// Usage
|
||||
const files = ['./interview1.wav', './interview2.wav', './interview3.wav'];
|
||||
const transcriptions = await transcribeBatch(files);
|
||||
|
||||
transcriptions.forEach(result => {
|
||||
if (result.success) {
|
||||
console.log(`${result.file}: ${result.transcription}`);
|
||||
} else {
|
||||
console.error(`${result.file}: Error - ${result.error}`);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Advanced Use Cases
|
||||
|
||||
### Audio File Processing with Metadata
|
||||
|
||||
```javascript
|
||||
import ZAI from 'z-ai-web-dev-sdk';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
async function transcribeWithMetadata(audioFilePath) {
|
||||
const zai = await ZAI.create();
|
||||
|
||||
// Get file metadata
|
||||
const stats = fs.statSync(audioFilePath);
|
||||
const audioFile = fs.readFileSync(audioFilePath);
|
||||
const base64Audio = audioFile.toString('base64');
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
const response = await zai.audio.asr.create({
|
||||
file_base64: base64Audio
|
||||
});
|
||||
|
||||
const endTime = Date.now();
|
||||
|
||||
return {
|
||||
filename: path.basename(audioFilePath),
|
||||
filepath: audioFilePath,
|
||||
fileSize: stats.size,
|
||||
transcription: response.text,
|
||||
wordCount: response.text.split(/\s+/).length,
|
||||
processingTime: endTime - startTime,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
|
||||
// Usage
|
||||
const result = await transcribeWithMetadata('./meeting_recording.wav');
|
||||
console.log('Transcription Details:', JSON.stringify(result, null, 2));
|
||||
```
|
||||
|
||||
### Real-time Audio Processing Service
|
||||
|
||||
```javascript
|
||||
import ZAI from 'z-ai-web-dev-sdk';
|
||||
import fs from 'fs';
|
||||
|
||||
class ASRService {
|
||||
constructor() {
|
||||
this.zai = null;
|
||||
this.transcriptionCache = new Map();
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
this.zai = await ZAI.create();
|
||||
}
|
||||
|
||||
generateCacheKey(audioBuffer) {
|
||||
const crypto = require('crypto');
|
||||
return crypto.createHash('md5').update(audioBuffer).digest('hex');
|
||||
}
|
||||
|
||||
async transcribe(audioFilePath, useCache = true) {
|
||||
const audioBuffer = fs.readFileSync(audioFilePath);
|
||||
const cacheKey = this.generateCacheKey(audioBuffer);
|
||||
|
||||
// Check cache
|
||||
if (useCache && this.transcriptionCache.has(cacheKey)) {
|
||||
return {
|
||||
transcription: this.transcriptionCache.get(cacheKey),
|
||||
cached: true
|
||||
};
|
||||
}
|
||||
|
||||
// Transcribe audio
|
||||
const base64Audio = audioBuffer.toString('base64');
|
||||
|
||||
const response = await this.zai.audio.asr.create({
|
||||
file_base64: base64Audio
|
||||
});
|
||||
|
||||
// Cache result
|
||||
if (useCache) {
|
||||
this.transcriptionCache.set(cacheKey, response.text);
|
||||
}
|
||||
|
||||
return {
|
||||
transcription: response.text,
|
||||
cached: false
|
||||
};
|
||||
}
|
||||
|
||||
clearCache() {
|
||||
this.transcriptionCache.clear();
|
||||
}
|
||||
|
||||
getCacheSize() {
|
||||
return this.transcriptionCache.size;
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
const asrService = new ASRService();
|
||||
await asrService.initialize();
|
||||
|
||||
const result1 = await asrService.transcribe('./audio.wav');
|
||||
console.log('First call (not cached):', result1);
|
||||
|
||||
const result2 = await asrService.transcribe('./audio.wav');
|
||||
console.log('Second call (cached):', result2);
|
||||
```
|
||||
|
||||
### Directory Transcription
|
||||
|
||||
```javascript
|
||||
import ZAI from 'z-ai-web-dev-sdk';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
async function transcribeDirectory(directoryPath, outputJsonPath) {
|
||||
const zai = await ZAI.create();
|
||||
|
||||
// Get all audio files
|
||||
const files = fs.readdirSync(directoryPath);
|
||||
const audioFiles = files.filter(file =>
|
||||
/\.(wav|mp3|m4a|flac|ogg)$/i.test(file)
|
||||
);
|
||||
|
||||
const results = {
|
||||
directory: directoryPath,
|
||||
totalFiles: audioFiles.length,
|
||||
processedAt: new Date().toISOString(),
|
||||
transcriptions: []
|
||||
};
|
||||
|
||||
for (const filename of audioFiles) {
|
||||
const filePath = path.join(directoryPath, filename);
|
||||
|
||||
try {
|
||||
const audioFile = fs.readFileSync(filePath);
|
||||
const base64Audio = audioFile.toString('base64');
|
||||
|
||||
const response = await zai.audio.asr.create({
|
||||
file_base64: base64Audio
|
||||
});
|
||||
|
||||
results.transcriptions.push({
|
||||
filename: filename,
|
||||
success: true,
|
||||
text: response.text,
|
||||
wordCount: response.text.split(/\s+/).length
|
||||
});
|
||||
|
||||
console.log(`✓ Transcribed: ${filename}`);
|
||||
} catch (error) {
|
||||
results.transcriptions.push({
|
||||
filename: filename,
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
|
||||
console.error(`✗ Failed: ${filename} - ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Save results to JSON
|
||||
fs.writeFileSync(
|
||||
outputJsonPath,
|
||||
JSON.stringify(results, null, 2)
|
||||
);
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
// Usage
|
||||
const results = await transcribeDirectory(
|
||||
'./audio-recordings',
|
||||
'./transcriptions.json'
|
||||
);
|
||||
|
||||
console.log(`\nProcessed ${results.totalFiles} files`);
|
||||
console.log(`Successful: ${results.transcriptions.filter(t => t.success).length}`);
|
||||
console.log(`Failed: ${results.transcriptions.filter(t => !t.success).length}`);
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Audio Format Handling
|
||||
|
||||
```javascript
|
||||
import ZAI from 'z-ai-web-dev-sdk';
|
||||
import fs from 'fs';
|
||||
|
||||
async function transcribeAnyFormat(audioFilePath) {
|
||||
// Supported formats: WAV, MP3, M4A, FLAC, OGG, etc.
|
||||
const validExtensions = ['.wav', '.mp3', '.m4a', '.flac', '.ogg'];
|
||||
const ext = audioFilePath.toLowerCase().substring(audioFilePath.lastIndexOf('.'));
|
||||
|
||||
if (!validExtensions.includes(ext)) {
|
||||
throw new Error(`Unsupported audio format: ${ext}`);
|
||||
}
|
||||
|
||||
const zai = await ZAI.create();
|
||||
const audioFile = fs.readFileSync(audioFilePath);
|
||||
const base64Audio = audioFile.toString('base64');
|
||||
|
||||
const response = await zai.audio.asr.create({
|
||||
file_base64: base64Audio
|
||||
});
|
||||
|
||||
return response.text;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Error Handling
|
||||
|
||||
```javascript
|
||||
import ZAI from 'z-ai-web-dev-sdk';
|
||||
import fs from 'fs';
|
||||
|
||||
async function safeTranscribe(audioFilePath) {
|
||||
try {
|
||||
// Validate file exists
|
||||
if (!fs.existsSync(audioFilePath)) {
|
||||
throw new Error(`File not found: ${audioFilePath}`);
|
||||
}
|
||||
|
||||
// Check file size (e.g., limit to 100MB)
|
||||
const stats = fs.statSync(audioFilePath);
|
||||
const fileSizeMB = stats.size / (1024 * 1024);
|
||||
|
||||
if (fileSizeMB > 100) {
|
||||
throw new Error(`File too large: ${fileSizeMB.toFixed(2)}MB (max 100MB)`);
|
||||
}
|
||||
|
||||
// Transcribe
|
||||
const zai = await ZAI.create();
|
||||
const audioFile = fs.readFileSync(audioFilePath);
|
||||
const base64Audio = audioFile.toString('base64');
|
||||
|
||||
const response = await zai.audio.asr.create({
|
||||
file_base64: base64Audio
|
||||
});
|
||||
|
||||
if (!response.text || response.text.trim().length === 0) {
|
||||
throw new Error('Empty transcription result');
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
transcription: response.text,
|
||||
filePath: audioFilePath,
|
||||
fileSize: stats.size
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Transcription error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
filePath: audioFilePath
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Post-Processing Transcriptions
|
||||
|
||||
```javascript
|
||||
function cleanTranscription(text) {
|
||||
// Remove excessive whitespace
|
||||
text = text.replace(/\s+/g, ' ').trim();
|
||||
|
||||
// Capitalize first letter of sentences
|
||||
text = text.replace(/(^\w|[.!?]\s+\w)/g, match => match.toUpperCase());
|
||||
|
||||
// Remove filler words (optional)
|
||||
const fillers = ['um', 'uh', 'ah', 'like', 'you know'];
|
||||
const fillerPattern = new RegExp(`\\b(${fillers.join('|')})\\b`, 'gi');
|
||||
text = text.replace(fillerPattern, '').replace(/\s+/g, ' ');
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
async function transcribeAndClean(audioFilePath) {
|
||||
const zai = await ZAI.create();
|
||||
|
||||
const audioFile = fs.readFileSync(audioFilePath);
|
||||
const base64Audio = audioFile.toString('base64');
|
||||
|
||||
const response = await zai.audio.asr.create({
|
||||
file_base64: base64Audio
|
||||
});
|
||||
|
||||
return {
|
||||
raw: response.text,
|
||||
cleaned: cleanTranscription(response.text)
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Common Use Cases
|
||||
|
||||
1. **Meeting Transcription**: Convert recorded meetings into searchable text
|
||||
2. **Interview Processing**: Transcribe interviews for analysis and documentation
|
||||
3. **Podcast Transcription**: Create text versions of podcast episodes
|
||||
4. **Voice Notes**: Convert voice memos to text for easier reference
|
||||
5. **Call Center Analytics**: Analyze customer service calls
|
||||
6. **Accessibility**: Provide text alternatives for audio content
|
||||
7. **Voice Commands**: Enable voice-controlled applications
|
||||
8. **Language Learning**: Transcribe pronunciation practice
|
||||
|
||||
## Integration Examples
|
||||
|
||||
### Express.js API Endpoint
|
||||
|
||||
```javascript
|
||||
import express from 'express';
|
||||
import multer from 'multer';
|
||||
import ZAI from 'z-ai-web-dev-sdk';
|
||||
import fs from 'fs';
|
||||
|
||||
const app = express();
|
||||
const upload = multer({ dest: 'uploads/' });
|
||||
|
||||
let zaiInstance;
|
||||
|
||||
async function initZAI() {
|
||||
zaiInstance = await ZAI.create();
|
||||
}
|
||||
|
||||
app.post('/api/transcribe', upload.single('audio'), async (req, res) => {
|
||||
try {
|
||||
if (!req.file) {
|
||||
return res.status(400).json({ error: 'No audio file provided' });
|
||||
}
|
||||
|
||||
const audioFile = fs.readFileSync(req.file.path);
|
||||
const base64Audio = audioFile.toString('base64');
|
||||
|
||||
const response = await zaiInstance.audio.asr.create({
|
||||
file_base64: base64Audio
|
||||
});
|
||||
|
||||
// Clean up uploaded file
|
||||
fs.unlinkSync(req.file.path);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
transcription: response.text,
|
||||
wordCount: response.text.split(/\s+/).length
|
||||
});
|
||||
} catch (error) {
|
||||
// Clean up on error
|
||||
if (req.file && fs.existsSync(req.file.path)) {
|
||||
fs.unlinkSync(req.file.path);
|
||||
}
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
initZAI().then(() => {
|
||||
app.listen(3000, () => {
|
||||
console.log('ASR API running on port 3000');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Issue**: "SDK must be used in backend"
|
||||
- **Solution**: Ensure z-ai-web-dev-sdk is only imported in server-side code
|
||||
|
||||
**Issue**: Empty or incorrect transcription
|
||||
- **Solution**: Verify audio quality and format. Check if audio contains clear speech
|
||||
|
||||
**Issue**: Large file processing fails
|
||||
- **Solution**: Consider splitting large audio files into smaller segments
|
||||
|
||||
**Issue**: Slow transcription speed
|
||||
- **Solution**: Implement caching for repeated transcriptions, optimize file sizes
|
||||
|
||||
**Issue**: Memory errors with large files
|
||||
- **Solution**: Process files in chunks or increase Node.js memory limit
|
||||
|
||||
## Performance Tips
|
||||
|
||||
1. **Reuse SDK Instance**: Create once, use multiple times
|
||||
2. **Implement Caching**: Cache transcriptions for duplicate files
|
||||
3. **Batch Processing**: Process multiple files efficiently with proper queuing
|
||||
4. **Audio Optimization**: Compress audio files before processing when possible
|
||||
5. **Async Operations**: Use Promise.all for parallel processing when appropriate
|
||||
|
||||
## Audio Quality Guidelines
|
||||
|
||||
For best transcription results:
|
||||
- **Sample Rate**: 16kHz or higher
|
||||
- **Format**: WAV, MP3, or M4A recommended
|
||||
- **Noise Level**: Minimize background noise
|
||||
- **Speech Clarity**: Clear pronunciation and normal speaking pace
|
||||
- **File Size**: Under 100MB recommended for individual files
|
||||
|
||||
## Remember
|
||||
|
||||
- Always use z-ai-web-dev-sdk in backend code only
|
||||
- The SDK is already installed - import as shown in examples
|
||||
- Audio files must be converted to base64 before processing
|
||||
- Implement proper error handling for production applications
|
||||
- Consider audio quality for best transcription accuracy
|
||||
- Clean up temporary files after processing
|
||||
- Cache results for frequently transcribed files
|
||||
Executable
+27
@@ -0,0 +1,27 @@
|
||||
import ZAI from 'z-ai-web-dev-sdk';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
async function main(inputFile: string) {
|
||||
if (!fs.existsSync(inputFile)) {
|
||||
console.error(`Audio file not found: ${inputFile}`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const zai = await ZAI.create();
|
||||
|
||||
const audioBuffer = fs.readFileSync(inputFile);
|
||||
const file_base64 = audioBuffer.toString('base64');
|
||||
|
||||
const result = await zai.audio.asr.create({ file_base64 });
|
||||
|
||||
console.log('Transcription result:');
|
||||
console.log(result.text ?? JSON.stringify(result, null, 2));
|
||||
} catch (err: any) {
|
||||
console.error('ASR failed:', err?.message || err);
|
||||
}
|
||||
}
|
||||
|
||||
main('./output.wav');
|
||||
|
||||
Executable
+21
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 z-ai-web-dev-sdk Skills
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
Executable
+856
@@ -0,0 +1,856 @@
|
||||
---
|
||||
name: LLM
|
||||
description: Implement large language model (LLM) chat completions using the z-ai-web-dev-sdk. Use this skill when the user needs to build conversational AI applications, chatbots, AI assistants, or any text generation features. Supports multi-turn conversations, system prompts, and context management.
|
||||
license: MIT
|
||||
---
|
||||
|
||||
# LLM (Large Language Model) Skill
|
||||
|
||||
This skill guides the implementation of chat completions functionality using the z-ai-web-dev-sdk package, enabling powerful conversational AI and text generation capabilities.
|
||||
|
||||
## Skills Path
|
||||
|
||||
**Skill Location**: `{project_path}/skills/llm`
|
||||
|
||||
this skill is located at above path in your project.
|
||||
|
||||
**Reference Scripts**: Example test scripts are available in the `{Skill Location}/scripts/` directory for quick testing and reference. See `{Skill Location}/scripts/chat.ts` for a working example.
|
||||
|
||||
## Overview
|
||||
|
||||
The LLM skill allows you to build applications that leverage large language models for natural language understanding and generation, including chatbots, AI assistants, content generation, and more.
|
||||
|
||||
**IMPORTANT**: z-ai-web-dev-sdk MUST be used in backend code only. Never use it in client-side code.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
The z-ai-web-dev-sdk package is already installed. Import it as shown in the examples below.
|
||||
|
||||
## CLI Usage (For Simple Tasks)
|
||||
|
||||
For simple, one-off chat completions, you can use the z-ai CLI instead of writing code. This is ideal for quick tests, simple queries, or automation scripts.
|
||||
|
||||
### Basic Chat
|
||||
|
||||
```bash
|
||||
# Simple question
|
||||
z-ai chat --prompt "What is the capital of France?"
|
||||
|
||||
# Save response to file
|
||||
z-ai chat -p "Explain quantum computing" -o response.json
|
||||
|
||||
# Stream the response
|
||||
z-ai chat -p "Write a short poem" --stream
|
||||
```
|
||||
|
||||
### With System Prompt
|
||||
|
||||
```bash
|
||||
# Custom system prompt for specific behavior
|
||||
z-ai chat \
|
||||
--prompt "Review this code: function add(a,b) { return a+b; }" \
|
||||
--system "You are an expert code reviewer" \
|
||||
-o review.json
|
||||
```
|
||||
|
||||
### With Thinking (Chain of Thought)
|
||||
|
||||
```bash
|
||||
# Enable thinking for complex reasoning
|
||||
z-ai chat \
|
||||
--prompt "Solve this math problem: If a train travels 120km in 2 hours, what's its speed?" \
|
||||
--thinking \
|
||||
-o solution.json
|
||||
```
|
||||
|
||||
### CLI Parameters
|
||||
|
||||
- `--prompt, -p <text>`: **Required** - User message content
|
||||
- `--system, -s <text>`: Optional - System prompt for custom behavior
|
||||
- `--thinking, -t`: Optional - Enable chain-of-thought reasoning (default: disabled)
|
||||
- `--output, -o <path>`: Optional - Output file path (JSON format)
|
||||
- `--stream`: Optional - Stream the response in real-time
|
||||
|
||||
### When to Use CLI vs SDK
|
||||
|
||||
**Use CLI for:**
|
||||
- Quick one-off questions
|
||||
- Simple automation scripts
|
||||
- Testing prompts
|
||||
- Single-turn conversations
|
||||
|
||||
**Use SDK for:**
|
||||
- Multi-turn conversations with context
|
||||
- Custom conversation management
|
||||
- Integration with web applications
|
||||
- Complex chat workflows
|
||||
- Production applications
|
||||
|
||||
## Basic Chat Completions
|
||||
|
||||
### Simple Question and Answer
|
||||
|
||||
```javascript
|
||||
import ZAI from 'z-ai-web-dev-sdk';
|
||||
|
||||
async function askQuestion(question) {
|
||||
const zai = await ZAI.create();
|
||||
|
||||
const completion = await zai.chat.completions.create({
|
||||
messages: [
|
||||
{
|
||||
role: 'assistant',
|
||||
content: 'You are a helpful assistant.'
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: question
|
||||
}
|
||||
],
|
||||
thinking: { type: 'disabled' }
|
||||
});
|
||||
|
||||
const response = completion.choices[0]?.message?.content;
|
||||
return response;
|
||||
}
|
||||
|
||||
// Usage
|
||||
const answer = await askQuestion('What is the capital of France?');
|
||||
console.log('Answer:', answer);
|
||||
```
|
||||
|
||||
### Custom System Prompt
|
||||
|
||||
```javascript
|
||||
import ZAI from 'z-ai-web-dev-sdk';
|
||||
|
||||
async function customAssistant(systemPrompt, userMessage) {
|
||||
const zai = await ZAI.create();
|
||||
|
||||
const completion = await zai.chat.completions.create({
|
||||
messages: [
|
||||
{
|
||||
role: 'assistant',
|
||||
content: systemPrompt
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: userMessage
|
||||
}
|
||||
],
|
||||
thinking: { type: 'disabled' }
|
||||
});
|
||||
|
||||
return completion.choices[0]?.message?.content;
|
||||
}
|
||||
|
||||
// Usage - Code reviewer
|
||||
const codeReview = await customAssistant(
|
||||
'You are an expert code reviewer. Analyze code for bugs, performance issues, and best practices.',
|
||||
'Review this function: function add(a, b) { return a + b; }'
|
||||
);
|
||||
|
||||
// Usage - Creative writer
|
||||
const story = await customAssistant(
|
||||
'You are a creative fiction writer who writes engaging short stories.',
|
||||
'Write a short story about a robot learning to paint.'
|
||||
);
|
||||
|
||||
console.log(codeReview);
|
||||
console.log(story);
|
||||
```
|
||||
|
||||
## Multi-turn Conversations
|
||||
|
||||
### Conversation History Management
|
||||
|
||||
```javascript
|
||||
import ZAI from 'z-ai-web-dev-sdk';
|
||||
|
||||
class ConversationManager {
|
||||
constructor(systemPrompt = 'You are a helpful assistant.') {
|
||||
this.messages = [
|
||||
{
|
||||
role: 'assistant',
|
||||
content: systemPrompt
|
||||
}
|
||||
];
|
||||
this.zai = null;
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
this.zai = await ZAI.create();
|
||||
}
|
||||
|
||||
async sendMessage(userMessage) {
|
||||
// Add user message to history
|
||||
this.messages.push({
|
||||
role: 'user',
|
||||
content: userMessage
|
||||
});
|
||||
|
||||
// Get completion
|
||||
const completion = await this.zai.chat.completions.create({
|
||||
messages: this.messages,
|
||||
thinking: { type: 'disabled' }
|
||||
});
|
||||
|
||||
const assistantResponse = completion.choices[0]?.message?.content;
|
||||
|
||||
// Add assistant response to history
|
||||
this.messages.push({
|
||||
role: 'assistant',
|
||||
content: assistantResponse
|
||||
});
|
||||
|
||||
return assistantResponse;
|
||||
}
|
||||
|
||||
getHistory() {
|
||||
return this.messages;
|
||||
}
|
||||
|
||||
clearHistory(systemPrompt = 'You are a helpful assistant.') {
|
||||
this.messages = [
|
||||
{
|
||||
role: 'assistant',
|
||||
content: systemPrompt
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
getMessageCount() {
|
||||
// Subtract 1 for system message
|
||||
return this.messages.length - 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
const conversation = new ConversationManager();
|
||||
await conversation.initialize();
|
||||
|
||||
const response1 = await conversation.sendMessage('Hi, my name is John.');
|
||||
console.log('AI:', response1);
|
||||
|
||||
const response2 = await conversation.sendMessage('What is my name?');
|
||||
console.log('AI:', response2); // Should remember the name is John
|
||||
|
||||
console.log('Total messages:', conversation.getMessageCount());
|
||||
```
|
||||
|
||||
### Context-Aware Conversations
|
||||
|
||||
```javascript
|
||||
import ZAI from 'z-ai-web-dev-sdk';
|
||||
|
||||
class ContextualChat {
|
||||
constructor() {
|
||||
this.messages = [];
|
||||
this.zai = null;
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
this.zai = await ZAI.create();
|
||||
}
|
||||
|
||||
async startConversation(role, context) {
|
||||
// Set up system prompt with context
|
||||
const systemPrompt = `You are ${role}. Context: ${context}`;
|
||||
|
||||
this.messages = [
|
||||
{
|
||||
role: 'assistant',
|
||||
content: systemPrompt
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
async chat(userMessage) {
|
||||
this.messages.push({
|
||||
role: 'user',
|
||||
content: userMessage
|
||||
});
|
||||
|
||||
const completion = await this.zai.chat.completions.create({
|
||||
messages: this.messages,
|
||||
thinking: { type: 'disabled' }
|
||||
});
|
||||
|
||||
const response = completion.choices[0]?.message?.content;
|
||||
|
||||
this.messages.push({
|
||||
role: 'assistant',
|
||||
content: response
|
||||
});
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
// Usage - Customer support scenario
|
||||
const support = new ContextualChat();
|
||||
await support.initialize();
|
||||
|
||||
await support.startConversation(
|
||||
'a customer support agent for TechCorp',
|
||||
'The user has ordered product #12345 which is delayed due to shipping issues.'
|
||||
);
|
||||
|
||||
const reply1 = await support.chat('Where is my order?');
|
||||
console.log('Support:', reply1);
|
||||
|
||||
const reply2 = await support.chat('Can I get a refund?');
|
||||
console.log('Support:', reply2);
|
||||
```
|
||||
|
||||
## Advanced Use Cases
|
||||
|
||||
### Content Generation
|
||||
|
||||
```javascript
|
||||
import ZAI from 'z-ai-web-dev-sdk';
|
||||
|
||||
class ContentGenerator {
|
||||
constructor() {
|
||||
this.zai = null;
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
this.zai = await ZAI.create();
|
||||
}
|
||||
|
||||
async generateBlogPost(topic, tone = 'professional') {
|
||||
const completion = await this.zai.chat.completions.create({
|
||||
messages: [
|
||||
{
|
||||
role: 'assistant',
|
||||
content: `You are a professional content writer. Write in a ${tone} tone.`
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: `Write a blog post about: ${topic}. Include an introduction, main points, and conclusion.`
|
||||
}
|
||||
],
|
||||
thinking: { type: 'disabled' }
|
||||
});
|
||||
|
||||
return completion.choices[0]?.message?.content;
|
||||
}
|
||||
|
||||
async generateProductDescription(productName, features) {
|
||||
const completion = await this.zai.chat.completions.create({
|
||||
messages: [
|
||||
{
|
||||
role: 'assistant',
|
||||
content: 'You are an expert at writing compelling product descriptions for e-commerce.'
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: `Write a product description for "${productName}". Key features: ${features.join(', ')}.`
|
||||
}
|
||||
],
|
||||
thinking: { type: 'disabled' }
|
||||
});
|
||||
|
||||
return completion.choices[0]?.message?.content;
|
||||
}
|
||||
|
||||
async generateEmailResponse(originalEmail, intent) {
|
||||
const completion = await this.zai.chat.completions.create({
|
||||
messages: [
|
||||
{
|
||||
role: 'assistant',
|
||||
content: 'You are a professional email writer. Write clear, concise, and polite emails.'
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: `Original email: "${originalEmail}"\n\nWrite a ${intent} response.`
|
||||
}
|
||||
],
|
||||
thinking: { type: 'disabled' }
|
||||
});
|
||||
|
||||
return completion.choices[0]?.message?.content;
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
const generator = new ContentGenerator();
|
||||
await generator.initialize();
|
||||
|
||||
const blogPost = await generator.generateBlogPost(
|
||||
'The Future of Artificial Intelligence',
|
||||
'informative'
|
||||
);
|
||||
console.log('Blog Post:', blogPost);
|
||||
|
||||
const productDesc = await generator.generateProductDescription(
|
||||
'Smart Watch Pro',
|
||||
['Heart rate monitoring', 'GPS tracking', 'Waterproof', '7-day battery life']
|
||||
);
|
||||
console.log('Product Description:', productDesc);
|
||||
```
|
||||
|
||||
### Data Analysis and Summarization
|
||||
|
||||
```javascript
|
||||
import ZAI from 'z-ai-web-dev-sdk';
|
||||
|
||||
async function analyzeData(data, analysisType) {
|
||||
const zai = await ZAI.create();
|
||||
|
||||
const prompts = {
|
||||
summarize: 'You are a data analyst. Summarize the key insights from the data.',
|
||||
trend: 'You are a data analyst. Identify trends and patterns in the data.',
|
||||
recommendation: 'You are a business analyst. Provide actionable recommendations based on the data.'
|
||||
};
|
||||
|
||||
const completion = await zai.chat.completions.create({
|
||||
messages: [
|
||||
{
|
||||
role: 'assistant',
|
||||
content: prompts[analysisType] || prompts.summarize
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: `Analyze this data:\n\n${JSON.stringify(data, null, 2)}`
|
||||
}
|
||||
],
|
||||
thinking: { type: 'disabled' }
|
||||
});
|
||||
|
||||
return completion.choices[0]?.message?.content;
|
||||
}
|
||||
|
||||
// Usage
|
||||
const salesData = {
|
||||
Q1: { revenue: 100000, customers: 250 },
|
||||
Q2: { revenue: 120000, customers: 280 },
|
||||
Q3: { revenue: 150000, customers: 320 },
|
||||
Q4: { revenue: 180000, customers: 380 }
|
||||
};
|
||||
|
||||
const summary = await analyzeData(salesData, 'summarize');
|
||||
const trends = await analyzeData(salesData, 'trend');
|
||||
const recommendations = await analyzeData(salesData, 'recommendation');
|
||||
|
||||
console.log('Summary:', summary);
|
||||
console.log('Trends:', trends);
|
||||
console.log('Recommendations:', recommendations);
|
||||
```
|
||||
|
||||
### Code Generation and Debugging
|
||||
|
||||
```javascript
|
||||
import ZAI from 'z-ai-web-dev-sdk';
|
||||
|
||||
class CodeAssistant {
|
||||
constructor() {
|
||||
this.zai = null;
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
this.zai = await ZAI.create();
|
||||
}
|
||||
|
||||
async generateCode(description, language) {
|
||||
const completion = await this.zai.chat.completions.create({
|
||||
messages: [
|
||||
{
|
||||
role: 'assistant',
|
||||
content: `You are an expert ${language} programmer. Write clean, efficient, and well-commented code.`
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: `Write ${language} code to: ${description}`
|
||||
}
|
||||
],
|
||||
thinking: { type: 'disabled' }
|
||||
});
|
||||
|
||||
return completion.choices[0]?.message?.content;
|
||||
}
|
||||
|
||||
async debugCode(code, issue) {
|
||||
const completion = await this.zai.chat.completions.create({
|
||||
messages: [
|
||||
{
|
||||
role: 'assistant',
|
||||
content: 'You are an expert debugger. Identify bugs and suggest fixes.'
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: `Code:\n${code}\n\nIssue: ${issue}\n\nFind the bug and suggest a fix.`
|
||||
}
|
||||
],
|
||||
thinking: { type: 'disabled' }
|
||||
});
|
||||
|
||||
return completion.choices[0]?.message?.content;
|
||||
}
|
||||
|
||||
async explainCode(code) {
|
||||
const completion = await this.zai.chat.completions.create({
|
||||
messages: [
|
||||
{
|
||||
role: 'assistant',
|
||||
content: 'You are a programming teacher. Explain code clearly and simply.'
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: `Explain what this code does:\n\n${code}`
|
||||
}
|
||||
],
|
||||
thinking: { type: 'disabled' }
|
||||
});
|
||||
|
||||
return completion.choices[0]?.message?.content;
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
const codeAssist = new CodeAssistant();
|
||||
await codeAssist.initialize();
|
||||
|
||||
const newCode = await codeAssist.generateCode(
|
||||
'Create a function that sorts an array of objects by a specific property',
|
||||
'JavaScript'
|
||||
);
|
||||
console.log('Generated Code:', newCode);
|
||||
|
||||
const bugFix = await codeAssist.debugCode(
|
||||
'function add(a, b) { return a - b; }',
|
||||
'This function should add numbers but returns wrong results'
|
||||
);
|
||||
console.log('Debug Suggestion:', bugFix);
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Prompt Engineering
|
||||
|
||||
```javascript
|
||||
// Bad: Vague prompt
|
||||
const bad = await askQuestion('Tell me about AI');
|
||||
|
||||
// Good: Specific and structured prompt
|
||||
async function askWithContext(topic, format, audience) {
|
||||
const zai = await ZAI.create();
|
||||
|
||||
const completion = await zai.chat.completions.create({
|
||||
messages: [
|
||||
{
|
||||
role: 'assistant',
|
||||
content: `You are an expert educator. Explain topics clearly for ${audience}.`
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: `Explain ${topic} in ${format} format. Include practical examples.`
|
||||
}
|
||||
],
|
||||
thinking: { type: 'disabled' }
|
||||
});
|
||||
|
||||
return completion.choices[0]?.message?.content;
|
||||
}
|
||||
|
||||
const good = await askWithContext('artificial intelligence', 'bullet points', 'beginners');
|
||||
```
|
||||
|
||||
### 2. Error Handling
|
||||
|
||||
```javascript
|
||||
import ZAI from 'z-ai-web-dev-sdk';
|
||||
|
||||
async function safeCompletion(messages, retries = 3) {
|
||||
let lastError;
|
||||
|
||||
for (let attempt = 1; attempt <= retries; attempt++) {
|
||||
try {
|
||||
const zai = await ZAI.create();
|
||||
|
||||
const completion = await zai.chat.completions.create({
|
||||
messages: messages,
|
||||
thinking: { type: 'disabled' }
|
||||
});
|
||||
|
||||
const response = completion.choices[0]?.message?.content;
|
||||
|
||||
if (!response || response.trim().length === 0) {
|
||||
throw new Error('Empty response from AI');
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
content: response,
|
||||
attempts: attempt
|
||||
};
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
console.error(`Attempt ${attempt} failed:`, error.message);
|
||||
|
||||
if (attempt < retries) {
|
||||
// Wait before retry (exponential backoff)
|
||||
await new Promise(resolve => setTimeout(resolve, 1000 * attempt));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: lastError.message,
|
||||
attempts: retries
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Context Management
|
||||
|
||||
```javascript
|
||||
class ManagedConversation {
|
||||
constructor(maxMessages = 20) {
|
||||
this.maxMessages = maxMessages;
|
||||
this.systemPrompt = '';
|
||||
this.messages = [];
|
||||
this.zai = null;
|
||||
}
|
||||
|
||||
async initialize(systemPrompt) {
|
||||
this.zai = await ZAI.create();
|
||||
this.systemPrompt = systemPrompt;
|
||||
this.messages = [
|
||||
{
|
||||
role: 'assistant',
|
||||
content: systemPrompt
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
async chat(userMessage) {
|
||||
// Add user message
|
||||
this.messages.push({
|
||||
role: 'user',
|
||||
content: userMessage
|
||||
});
|
||||
|
||||
// Trim old messages if exceeding limit (keep system prompt)
|
||||
if (this.messages.length > this.maxMessages) {
|
||||
this.messages = [
|
||||
this.messages[0], // Keep system prompt
|
||||
...this.messages.slice(-(this.maxMessages - 1))
|
||||
];
|
||||
}
|
||||
|
||||
const completion = await this.zai.chat.completions.create({
|
||||
messages: this.messages,
|
||||
thinking: { type: 'disabled' }
|
||||
});
|
||||
|
||||
const response = completion.choices[0]?.message?.content;
|
||||
|
||||
this.messages.push({
|
||||
role: 'assistant',
|
||||
content: response
|
||||
});
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
getTokenEstimate() {
|
||||
// Rough estimate: ~4 characters per token
|
||||
const totalChars = this.messages
|
||||
.map(m => m.content.length)
|
||||
.reduce((a, b) => a + b, 0);
|
||||
return Math.ceil(totalChars / 4);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Response Processing
|
||||
|
||||
```javascript
|
||||
async function getStructuredResponse(query, format = 'json') {
|
||||
const zai = await ZAI.create();
|
||||
|
||||
const formatInstructions = {
|
||||
json: 'Respond with valid JSON only. No additional text.',
|
||||
list: 'Respond with a numbered list.',
|
||||
markdown: 'Respond in Markdown format.'
|
||||
};
|
||||
|
||||
const completion = await zai.chat.completions.create({
|
||||
messages: [
|
||||
{
|
||||
role: 'assistant',
|
||||
content: `You are a helpful assistant. ${formatInstructions[format]}`
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: query
|
||||
}
|
||||
],
|
||||
thinking: { type: 'disabled' }
|
||||
});
|
||||
|
||||
const response = completion.choices[0]?.message?.content;
|
||||
|
||||
// Parse JSON if requested
|
||||
if (format === 'json') {
|
||||
try {
|
||||
return JSON.parse(response);
|
||||
} catch (e) {
|
||||
console.error('Failed to parse JSON response');
|
||||
return { raw: response };
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
// Usage
|
||||
const jsonData = await getStructuredResponse(
|
||||
'List three programming languages with their primary use cases',
|
||||
'json'
|
||||
);
|
||||
console.log(jsonData);
|
||||
```
|
||||
|
||||
## Common Use Cases
|
||||
|
||||
1. **Chatbots & Virtual Assistants**: Build conversational interfaces for customer support
|
||||
2. **Content Generation**: Create articles, product descriptions, marketing copy
|
||||
3. **Code Assistance**: Generate, explain, and debug code
|
||||
4. **Data Analysis**: Analyze and summarize complex data sets
|
||||
5. **Language Translation**: Translate text between languages
|
||||
6. **Educational Tools**: Create tutoring and learning applications
|
||||
7. **Email Automation**: Generate professional email responses
|
||||
8. **Creative Writing**: Story generation, poetry, and creative content
|
||||
|
||||
## Integration Examples
|
||||
|
||||
### Express.js Chatbot API
|
||||
|
||||
```javascript
|
||||
import express from 'express';
|
||||
import ZAI from 'z-ai-web-dev-sdk';
|
||||
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
|
||||
// Store conversations in memory (use database in production)
|
||||
const conversations = new Map();
|
||||
|
||||
let zaiInstance;
|
||||
|
||||
async function initZAI() {
|
||||
zaiInstance = await ZAI.create();
|
||||
}
|
||||
|
||||
app.post('/api/chat', async (req, res) => {
|
||||
try {
|
||||
const { sessionId, message, systemPrompt } = req.body;
|
||||
|
||||
if (!message) {
|
||||
return res.status(400).json({ error: 'Message is required' });
|
||||
}
|
||||
|
||||
// Get or create conversation history
|
||||
let history = conversations.get(sessionId) || [
|
||||
{
|
||||
role: 'assistant',
|
||||
content: systemPrompt || 'You are a helpful assistant.'
|
||||
}
|
||||
];
|
||||
|
||||
// Add user message
|
||||
history.push({
|
||||
role: 'user',
|
||||
content: message
|
||||
});
|
||||
|
||||
// Get completion
|
||||
const completion = await zaiInstance.chat.completions.create({
|
||||
messages: history,
|
||||
thinking: { type: 'disabled' }
|
||||
});
|
||||
|
||||
const aiResponse = completion.choices[0]?.message?.content;
|
||||
|
||||
// Add AI response to history
|
||||
history.push({
|
||||
role: 'assistant',
|
||||
content: aiResponse
|
||||
});
|
||||
|
||||
// Save updated history
|
||||
conversations.set(sessionId, history);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
response: aiResponse,
|
||||
messageCount: history.length - 1
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
app.delete('/api/chat/:sessionId', (req, res) => {
|
||||
const { sessionId } = req.params;
|
||||
conversations.delete(sessionId);
|
||||
res.json({ success: true, message: 'Conversation cleared' });
|
||||
});
|
||||
|
||||
initZAI().then(() => {
|
||||
app.listen(3000, () => {
|
||||
console.log('Chatbot API running on port 3000');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Issue**: "SDK must be used in backend"
|
||||
- **Solution**: Ensure z-ai-web-dev-sdk is only imported and used in server-side code
|
||||
|
||||
**Issue**: Empty or incomplete responses
|
||||
- **Solution**: Check that completion.choices[0]?.message?.content exists and is not empty
|
||||
|
||||
**Issue**: Conversation context getting too long
|
||||
- **Solution**: Implement message trimming to keep only recent messages
|
||||
|
||||
**Issue**: Inconsistent responses
|
||||
- **Solution**: Use more specific system prompts and provide clear instructions
|
||||
|
||||
**Issue**: Rate limiting errors
|
||||
- **Solution**: Implement retry logic with exponential backoff
|
||||
|
||||
## Performance Tips
|
||||
|
||||
1. **Reuse SDK Instance**: Create ZAI instance once and reuse across requests
|
||||
2. **Manage Context Length**: Trim old messages to avoid token limits
|
||||
3. **Implement Caching**: Cache responses for common queries
|
||||
4. **Use Specific Prompts**: Clear prompts lead to faster, better responses
|
||||
5. **Handle Errors Gracefully**: Implement retry logic and fallback responses
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. **Input Validation**: Always validate and sanitize user input
|
||||
2. **Rate Limiting**: Implement rate limits to prevent abuse
|
||||
3. **API Key Protection**: Never expose SDK credentials in client-side code
|
||||
4. **Content Filtering**: Filter sensitive or inappropriate content
|
||||
5. **Session Management**: Implement proper session handling and cleanup
|
||||
|
||||
## Remember
|
||||
|
||||
- Always use z-ai-web-dev-sdk in backend code only
|
||||
- The SDK is already installed - import as shown in examples
|
||||
- Use the 'assistant' role for system prompts
|
||||
- Set thinking to { type: 'disabled' } for standard completions
|
||||
- Implement proper error handling and retries for production
|
||||
- Manage conversation history to avoid token limits
|
||||
- Clear and specific prompts lead to better results
|
||||
- Check `scripts/chat.ts` for a quick start example
|
||||
Executable
+32
@@ -0,0 +1,32 @@
|
||||
import ZAI, { ChatMessage } from "z-ai-web-dev-sdk";
|
||||
|
||||
async function main(prompt: string) {
|
||||
try {
|
||||
const zai = await ZAI.create();
|
||||
|
||||
const messages: ChatMessage[] = [
|
||||
{
|
||||
role: "assistant",
|
||||
content: "Hi, I'm a helpful assistant."
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: prompt,
|
||||
},
|
||||
];
|
||||
|
||||
const response = await zai.chat.completions.create({
|
||||
messages,
|
||||
stream: false,
|
||||
thinking: { type: "disabled" },
|
||||
});
|
||||
|
||||
const reply = response.choices?.[0]?.message?.content;
|
||||
console.log("Chat reply:");
|
||||
console.log(reply ?? JSON.stringify(response, null, 2));
|
||||
} catch (err: any) {
|
||||
console.error("Chat failed:", err?.message || err);
|
||||
}
|
||||
}
|
||||
|
||||
main('What is the capital of France?');
|
||||
Executable
+21
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 z-ai-web-dev-sdk Skills
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
Executable
+735
@@ -0,0 +1,735 @@
|
||||
---
|
||||
name: TTS
|
||||
description: Implement text-to-speech (TTS) capabilities using the z-ai-web-dev-sdk. Use this skill when the user needs to convert text into natural-sounding speech, create audio content, build voice-enabled applications, or generate spoken audio files. Supports multiple voices, adjustable speed, and various audio formats.
|
||||
license: MIT
|
||||
---
|
||||
|
||||
# TTS (Text to Speech) Skill
|
||||
|
||||
This skill guides the implementation of text-to-speech (TTS) functionality using the z-ai-web-dev-sdk package, enabling conversion of text into natural-sounding speech audio.
|
||||
|
||||
## Skills Path
|
||||
|
||||
**Skill Location**: `{project_path}/skills/TTS`
|
||||
|
||||
This skill is located at the above path in your project.
|
||||
|
||||
**Reference Scripts**: Example test scripts are available in the `{Skill Location}/scripts/` directory for quick testing and reference. See `{Skill Location}/scripts/tts.ts` for a working example.
|
||||
|
||||
## Overview
|
||||
|
||||
Text-to-Speech allows you to build applications that generate spoken audio from text input, supporting various voices, speeds, and output formats for diverse use cases.
|
||||
|
||||
**IMPORTANT**: z-ai-web-dev-sdk MUST be used in backend code only. Never use it in client-side code.
|
||||
|
||||
## API Limitations and Constraints
|
||||
|
||||
Before implementing TTS functionality, be aware of these important limitations:
|
||||
|
||||
### Input Text Constraints
|
||||
- **Maximum length**: 1024 characters per request
|
||||
- Text exceeding this limit must be split into smaller chunks
|
||||
|
||||
### Audio Parameters
|
||||
- **Speed range**: 0.5 to 2.0
|
||||
- 0.5 = half speed (slower)
|
||||
- 1.0 = normal speed (default)
|
||||
- 2.0 = double speed (faster)
|
||||
- **Volume range**: Greater than 0, up to 10
|
||||
- Default: 1.0
|
||||
- Values must be greater than 0 (exclusive) and up to 10 (inclusive)
|
||||
|
||||
### Format and Streaming
|
||||
- **Streaming limitation**: When `stream: true` is enabled, only `pcm` format is supported
|
||||
- **Non-streaming**: Supports `wav`, `pcm`, and `mp3` formats
|
||||
- **Sample rate**: 24000 Hz (recommended)
|
||||
|
||||
### Best Practice for Long Text
|
||||
```javascript
|
||||
function splitTextIntoChunks(text, maxLength = 1000) {
|
||||
const chunks = [];
|
||||
const sentences = text.match(/[^.!?]+[.!?]+/g) || [text];
|
||||
|
||||
let currentChunk = '';
|
||||
for (const sentence of sentences) {
|
||||
if ((currentChunk + sentence).length <= maxLength) {
|
||||
currentChunk += sentence;
|
||||
} else {
|
||||
if (currentChunk) chunks.push(currentChunk.trim());
|
||||
currentChunk = sentence;
|
||||
}
|
||||
}
|
||||
if (currentChunk) chunks.push(currentChunk.trim());
|
||||
|
||||
return chunks;
|
||||
}
|
||||
```
|
||||
|
||||
## Prerequisites
|
||||
|
||||
The z-ai-web-dev-sdk package is already installed. Import it as shown in the examples below.
|
||||
|
||||
## CLI Usage (For Simple Tasks)
|
||||
|
||||
For simple text-to-speech conversions, you can use the z-ai CLI instead of writing code. This is ideal for quick audio generation, testing voices, or simple automation.
|
||||
|
||||
### Basic TTS
|
||||
|
||||
```bash
|
||||
# Convert text to speech (default WAV format)
|
||||
z-ai tts --input "Hello, world" --output ./hello.wav
|
||||
|
||||
# Using short options
|
||||
z-ai tts -i "Hello, world" -o ./hello.wav
|
||||
```
|
||||
|
||||
### Different Voices and Speed
|
||||
|
||||
```bash
|
||||
# Use specific voice
|
||||
z-ai tts -i "Welcome to our service" -o ./welcome.wav --voice tongtong
|
||||
|
||||
# Adjust speech speed (0.5-2.0)
|
||||
z-ai tts -i "This is faster speech" -o ./fast.wav --speed 1.5
|
||||
|
||||
# Slower speech
|
||||
z-ai tts -i "This is slower speech" -o ./slow.wav --speed 0.8
|
||||
```
|
||||
|
||||
### Different Output Formats
|
||||
|
||||
```bash
|
||||
# MP3 format
|
||||
z-ai tts -i "Hello World" -o ./hello.mp3 --format mp3
|
||||
|
||||
# WAV format (default)
|
||||
z-ai tts -i "Hello World" -o ./hello.wav --format wav
|
||||
|
||||
# PCM format
|
||||
z-ai tts -i "Hello World" -o ./hello.pcm --format pcm
|
||||
```
|
||||
|
||||
### Streaming Output
|
||||
|
||||
```bash
|
||||
# Stream audio generation
|
||||
z-ai tts -i "This is a longer text that will be streamed" -o ./stream.wav --stream
|
||||
```
|
||||
|
||||
### CLI Parameters
|
||||
|
||||
- `--input, -i <text>`: **Required** - Text to convert to speech (max 1024 characters)
|
||||
- `--output, -o <path>`: **Required** - Output audio file path
|
||||
- `--voice, -v <voice>`: Optional - Voice type (default: tongtong)
|
||||
- `--speed, -s <number>`: Optional - Speech speed, 0.5-2.0 (default: 1.0)
|
||||
- `--format, -f <format>`: Optional - Output format: wav, mp3, pcm (default: wav)
|
||||
- `--stream`: Optional - Enable streaming output (only supports pcm format)
|
||||
|
||||
### When to Use CLI vs SDK
|
||||
|
||||
**Use CLI for:**
|
||||
- Quick text-to-speech conversions
|
||||
- Testing different voices and speeds
|
||||
- Simple batch audio generation
|
||||
- Command-line automation scripts
|
||||
|
||||
**Use SDK for:**
|
||||
- Dynamic audio generation in applications
|
||||
- Integration with web services
|
||||
- Custom audio processing pipelines
|
||||
- Production applications with complex requirements
|
||||
|
||||
## Basic TTS Implementation
|
||||
|
||||
### Simple Text to Speech
|
||||
|
||||
```javascript
|
||||
import ZAI from 'z-ai-web-dev-sdk';
|
||||
import fs from 'fs';
|
||||
|
||||
async function textToSpeech(text, outputPath) {
|
||||
const zai = await ZAI.create();
|
||||
|
||||
const response = await zai.audio.tts.create({
|
||||
input: text,
|
||||
voice: 'tongtong',
|
||||
speed: 1.0,
|
||||
response_format: 'wav',
|
||||
stream: false
|
||||
});
|
||||
|
||||
// Get array buffer from Response object
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
const buffer = Buffer.from(new Uint8Array(arrayBuffer));
|
||||
|
||||
fs.writeFileSync(outputPath, buffer);
|
||||
console.log(`Audio saved to ${outputPath}`);
|
||||
return outputPath;
|
||||
}
|
||||
|
||||
// Usage
|
||||
await textToSpeech('Hello, world!', './output.wav');
|
||||
```
|
||||
|
||||
### Multiple Voice Options
|
||||
|
||||
```javascript
|
||||
import ZAI from 'z-ai-web-dev-sdk';
|
||||
import fs from 'fs';
|
||||
|
||||
async function generateWithVoice(text, voice, outputPath) {
|
||||
const zai = await ZAI.create();
|
||||
|
||||
const response = await zai.audio.tts.create({
|
||||
input: text,
|
||||
voice: voice, // Available voices: tongtong, chuichui, xiaochen, jam, kazi, douji, luodo
|
||||
speed: 1.0,
|
||||
response_format: 'wav',
|
||||
stream: false
|
||||
});
|
||||
|
||||
// Get array buffer from Response object
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
const buffer = Buffer.from(new Uint8Array(arrayBuffer));
|
||||
|
||||
fs.writeFileSync(outputPath, buffer);
|
||||
return outputPath;
|
||||
}
|
||||
|
||||
// Usage
|
||||
await generateWithVoice('Welcome to our service', 'tongtong', './welcome.wav');
|
||||
```
|
||||
|
||||
### Adjustable Speed
|
||||
|
||||
```javascript
|
||||
import ZAI from 'z-ai-web-dev-sdk';
|
||||
import fs from 'fs';
|
||||
|
||||
async function generateWithSpeed(text, speed, outputPath) {
|
||||
const zai = await ZAI.create();
|
||||
|
||||
// Speed range: 0.5 to 2.0 (API constraint)
|
||||
// 0.5 = half speed (slower)
|
||||
// 1.0 = normal speed (default)
|
||||
// 2.0 = double speed (faster)
|
||||
// Values outside this range will cause API errors
|
||||
|
||||
const response = await zai.audio.tts.create({
|
||||
input: text,
|
||||
voice: 'tongtong',
|
||||
speed: speed,
|
||||
response_format: 'wav',
|
||||
stream: false
|
||||
});
|
||||
|
||||
// Get array buffer from Response object
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
const buffer = Buffer.from(new Uint8Array(arrayBuffer));
|
||||
|
||||
fs.writeFileSync(outputPath, buffer);
|
||||
return outputPath;
|
||||
}
|
||||
|
||||
// Usage - slower narration
|
||||
await generateWithSpeed('This is an important announcement', 0.8, './slow.wav');
|
||||
|
||||
// Usage - faster narration
|
||||
await generateWithSpeed('Quick update', 1.3, './fast.wav');
|
||||
```
|
||||
|
||||
### Adjustable Volume
|
||||
|
||||
```javascript
|
||||
import ZAI from 'z-ai-web-dev-sdk';
|
||||
import fs from 'fs';
|
||||
|
||||
async function generateWithVolume(text, volume, outputPath) {
|
||||
const zai = await ZAI.create();
|
||||
|
||||
// Volume range: greater than 0, up to 10 (API constraint)
|
||||
// Values must be > 0 (exclusive) and <= 10 (inclusive)
|
||||
// Default: 1.0 (normal volume)
|
||||
|
||||
const response = await zai.audio.tts.create({
|
||||
input: text,
|
||||
voice: 'tongtong',
|
||||
speed: 1.0,
|
||||
volume: volume, // Optional parameter
|
||||
response_format: 'wav',
|
||||
stream: false
|
||||
});
|
||||
|
||||
// Get array buffer from Response object
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
const buffer = Buffer.from(new Uint8Array(arrayBuffer));
|
||||
|
||||
fs.writeFileSync(outputPath, buffer);
|
||||
return outputPath;
|
||||
}
|
||||
|
||||
// Usage - louder audio
|
||||
await generateWithVolume('This is an announcement', 5.0, './loud.wav');
|
||||
|
||||
// Usage - quieter audio
|
||||
await generateWithVolume('Whispered message', 0.5, './quiet.wav');
|
||||
```
|
||||
|
||||
## Advanced Use Cases
|
||||
|
||||
### Batch Processing
|
||||
|
||||
```javascript
|
||||
import ZAI from 'z-ai-web-dev-sdk';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
async function batchTextToSpeech(textArray, outputDir) {
|
||||
const zai = await ZAI.create();
|
||||
const results = [];
|
||||
|
||||
// Ensure output directory exists
|
||||
if (!fs.existsSync(outputDir)) {
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
}
|
||||
|
||||
for (let i = 0; i < textArray.length; i++) {
|
||||
try {
|
||||
const text = textArray[i];
|
||||
const outputPath = path.join(outputDir, `audio_${i + 1}.wav`);
|
||||
|
||||
const response = await zai.audio.tts.create({
|
||||
input: text,
|
||||
voice: 'tongtong',
|
||||
speed: 1.0,
|
||||
response_format: 'wav',
|
||||
stream: false
|
||||
});
|
||||
|
||||
// Get array buffer from Response object
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
const buffer = Buffer.from(new Uint8Array(arrayBuffer));
|
||||
|
||||
fs.writeFileSync(outputPath, buffer);
|
||||
results.push({
|
||||
success: true,
|
||||
text,
|
||||
path: outputPath
|
||||
});
|
||||
} catch (error) {
|
||||
results.push({
|
||||
success: false,
|
||||
text: textArray[i],
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
// Usage
|
||||
const texts = [
|
||||
'Welcome to chapter one',
|
||||
'Welcome to chapter two',
|
||||
'Welcome to chapter three'
|
||||
];
|
||||
|
||||
const results = await batchTextToSpeech(texts, './audio-output');
|
||||
console.log('Generated:', results.length, 'audio files');
|
||||
```
|
||||
|
||||
### Dynamic Content Generation
|
||||
|
||||
```javascript
|
||||
import ZAI from 'z-ai-web-dev-sdk';
|
||||
import fs from 'fs';
|
||||
|
||||
class TTSGenerator {
|
||||
constructor() {
|
||||
this.zai = null;
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
this.zai = await ZAI.create();
|
||||
}
|
||||
|
||||
async generateAudio(text, options = {}) {
|
||||
const {
|
||||
voice = 'tongtong',
|
||||
speed = 1.0,
|
||||
format = 'wav'
|
||||
} = options;
|
||||
|
||||
const response = await this.zai.audio.tts.create({
|
||||
input: text,
|
||||
voice: voice,
|
||||
speed: speed,
|
||||
response_format: format,
|
||||
stream: false
|
||||
});
|
||||
|
||||
// Get array buffer from Response object
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
return Buffer.from(new Uint8Array(arrayBuffer));
|
||||
}
|
||||
|
||||
async saveAudio(text, outputPath, options = {}) {
|
||||
const buffer = await this.generateAudio(text, options);
|
||||
if (buffer) {
|
||||
fs.writeFileSync(outputPath, buffer);
|
||||
return outputPath;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
const generator = new TTSGenerator();
|
||||
await generator.initialize();
|
||||
|
||||
await generator.saveAudio(
|
||||
'Hello, this is a test',
|
||||
'./output.wav',
|
||||
{ speed: 1.2 }
|
||||
);
|
||||
```
|
||||
|
||||
### Next.js API Route Example
|
||||
|
||||
```javascript
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const { text, voice = 'tongtong', speed = 1.0 } = await req.json();
|
||||
|
||||
// Import ZAI SDK
|
||||
const ZAI = (await import('z-ai-web-dev-sdk')).default;
|
||||
|
||||
// Create SDK instance
|
||||
const zai = await ZAI.create();
|
||||
|
||||
// Generate TTS audio
|
||||
const response = await zai.audio.tts.create({
|
||||
input: text.trim(),
|
||||
voice: voice,
|
||||
speed: speed,
|
||||
response_format: 'wav',
|
||||
stream: false,
|
||||
});
|
||||
|
||||
// Get array buffer from Response object
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
const buffer = Buffer.from(new Uint8Array(arrayBuffer));
|
||||
|
||||
// Return audio as response
|
||||
return new NextResponse(buffer, {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'audio/wav',
|
||||
'Content-Length': buffer.length.toString(),
|
||||
'Cache-Control': 'no-cache',
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('TTS API Error:', error);
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: error instanceof Error ? error.message : '生成语音失败,请稍后重试',
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Text Preparation
|
||||
```javascript
|
||||
function prepareTextForTTS(text) {
|
||||
// Remove excessive whitespace
|
||||
text = text.replace(/\s+/g, ' ').trim();
|
||||
|
||||
// Expand common abbreviations for better pronunciation
|
||||
const abbreviations = {
|
||||
'Dr.': 'Doctor',
|
||||
'Mr.': 'Mister',
|
||||
'Mrs.': 'Misses',
|
||||
'etc.': 'et cetera'
|
||||
};
|
||||
|
||||
for (const [abbr, full] of Object.entries(abbreviations)) {
|
||||
text = text.replace(new RegExp(abbr, 'g'), full);
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Error Handling
|
||||
```javascript
|
||||
import ZAI from 'z-ai-web-dev-sdk';
|
||||
import fs from 'fs';
|
||||
|
||||
async function safeTTS(text, outputPath) {
|
||||
try {
|
||||
// Validate input
|
||||
if (!text || text.trim().length === 0) {
|
||||
throw new Error('Text input cannot be empty');
|
||||
}
|
||||
|
||||
if (text.length > 1024) {
|
||||
throw new Error('Text input exceeds maximum length of 1024 characters');
|
||||
}
|
||||
|
||||
const zai = await ZAI.create();
|
||||
|
||||
const response = await zai.audio.tts.create({
|
||||
input: text,
|
||||
voice: 'tongtong',
|
||||
speed: 1.0,
|
||||
response_format: 'wav',
|
||||
stream: false
|
||||
});
|
||||
|
||||
// Get array buffer from Response object
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
const buffer = Buffer.from(new Uint8Array(arrayBuffer));
|
||||
|
||||
fs.writeFileSync(outputPath, buffer);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
path: outputPath,
|
||||
size: buffer.length
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('TTS Error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. SDK Instance Reuse
|
||||
|
||||
```javascript
|
||||
import ZAI from 'z-ai-web-dev-sdk';
|
||||
|
||||
// Create a singleton instance
|
||||
let zaiInstance = null;
|
||||
|
||||
async function getZAIInstance() {
|
||||
if (!zaiInstance) {
|
||||
zaiInstance = await ZAI.create();
|
||||
}
|
||||
return zaiInstance;
|
||||
}
|
||||
|
||||
// Usage
|
||||
const zai = await getZAIInstance();
|
||||
const response = await zai.audio.tts.create({ ... });
|
||||
```
|
||||
|
||||
## Common Use Cases
|
||||
|
||||
1. **Audiobooks & Podcasts**: Convert written content to audio format
|
||||
2. **E-learning**: Create narration for educational content
|
||||
3. **Accessibility**: Provide audio versions of text content
|
||||
4. **Voice Assistants**: Generate dynamic responses
|
||||
5. **Announcements**: Create automated audio notifications
|
||||
6. **IVR Systems**: Generate phone system prompts
|
||||
7. **Content Localization**: Create audio in different languages
|
||||
|
||||
## Integration Examples
|
||||
|
||||
### Express.js API Endpoint
|
||||
|
||||
```javascript
|
||||
import express from 'express';
|
||||
import ZAI from 'z-ai-web-dev-sdk';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
|
||||
let zaiInstance;
|
||||
const outputDir = './audio-output';
|
||||
|
||||
async function initZAI() {
|
||||
zaiInstance = await ZAI.create();
|
||||
if (!fs.existsSync(outputDir)) {
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
app.post('/api/tts', async (req, res) => {
|
||||
try {
|
||||
const { text, voice = 'tongtong', speed = 1.0 } = req.body;
|
||||
|
||||
if (!text) {
|
||||
return res.status(400).json({ error: 'Text is required' });
|
||||
}
|
||||
|
||||
const filename = `tts_${Date.now()}.wav`;
|
||||
const outputPath = path.join(outputDir, filename);
|
||||
|
||||
const response = await zaiInstance.audio.tts.create({
|
||||
input: text,
|
||||
voice: voice,
|
||||
speed: speed,
|
||||
response_format: 'wav',
|
||||
stream: false
|
||||
});
|
||||
|
||||
// Get array buffer from Response object
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
const buffer = Buffer.from(new Uint8Array(arrayBuffer));
|
||||
|
||||
fs.writeFileSync(outputPath, buffer);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
audioUrl: `/audio/${filename}`,
|
||||
size: buffer.length
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.use('/audio', express.static('audio-output'));
|
||||
|
||||
initZAI().then(() => {
|
||||
app.listen(3000, () => {
|
||||
console.log('TTS API running on port 3000');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Issue**: "Input text exceeds maximum length"
|
||||
- **Solution**: Text input is limited to 1024 characters. Split longer text into chunks using the `splitTextIntoChunks` function shown in the API Limitations section
|
||||
|
||||
**Issue**: "Invalid speed parameter" or unexpected speed behavior
|
||||
- **Solution**: Speed must be between 0.5 and 2.0. Check your speed value is within this range
|
||||
|
||||
**Issue**: "Invalid volume parameter"
|
||||
- **Solution**: Volume must be greater than 0 and up to 10. Ensure volume value is in range (0, 10]
|
||||
|
||||
**Issue**: "Stream format not supported" with WAV/MP3
|
||||
- **Solution**: Streaming mode only supports PCM format. Either use `response_format: 'pcm'` with streaming, or disable streaming (`stream: false`) for WAV/MP3 output
|
||||
|
||||
**Issue**: "SDK must be used in backend"
|
||||
- **Solution**: Ensure z-ai-web-dev-sdk is only imported in server-side code
|
||||
|
||||
**Issue**: "TypeError: response.audio is undefined"
|
||||
- **Solution**: The SDK returns a standard Response object, use `await response.arrayBuffer()` instead of accessing `response.audio`
|
||||
|
||||
**Issue**: Generated audio file is empty or corrupted
|
||||
- **Solution**: Ensure you're calling `await response.arrayBuffer()` and properly converting to Buffer: `Buffer.from(new Uint8Array(arrayBuffer))`
|
||||
|
||||
**Issue**: Audio sounds unnatural
|
||||
- **Solution**: Prepare text properly (remove special characters, expand abbreviations)
|
||||
|
||||
**Issue**: Long processing times
|
||||
- **Solution**: Break long text into smaller chunks and process in parallel
|
||||
|
||||
**Issue**: Next.js caching old API route
|
||||
- **Solution**: Create a new API route endpoint or restart the dev server
|
||||
|
||||
## Performance Tips
|
||||
|
||||
1. **Reuse SDK Instance**: Create ZAI instance once and reuse
|
||||
2. **Implement Caching**: Cache generated audio for repeated text
|
||||
3. **Batch Processing**: Process multiple texts efficiently
|
||||
4. **Optimize Text**: Remove unnecessary content before generation
|
||||
5. **Async Processing**: Use queues for handling multiple requests
|
||||
|
||||
## Important Notes
|
||||
|
||||
### API Constraints
|
||||
|
||||
**Input Text Length**: Maximum 1024 characters per request. For longer text:
|
||||
```javascript
|
||||
// Split long text into chunks
|
||||
const longText = "..."; // Your long text here
|
||||
const chunks = splitTextIntoChunks(longText, 1000);
|
||||
|
||||
for (const chunk of chunks) {
|
||||
const response = await zai.audio.tts.create({
|
||||
input: chunk,
|
||||
voice: 'tongtong',
|
||||
speed: 1.0,
|
||||
response_format: 'wav',
|
||||
stream: false
|
||||
});
|
||||
// Process each chunk...
|
||||
}
|
||||
```
|
||||
|
||||
**Streaming Format Limitation**: When using `stream: true`, only `pcm` format is supported. For `wav` or `mp3` output, use `stream: false`.
|
||||
|
||||
**Sample Rate**: Audio is generated at 24000 Hz sample rate (recommended setting for playback).
|
||||
|
||||
### Response Object Format
|
||||
|
||||
The `zai.audio.tts.create()` method returns a standard **Response** object (not a custom object with an `audio` property). Always use:
|
||||
|
||||
```javascript
|
||||
// ✅ CORRECT
|
||||
const response = await zai.audio.tts.create({ ... });
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
const buffer = Buffer.from(new Uint8Array(arrayBuffer));
|
||||
|
||||
// ❌ WRONG - This will not work
|
||||
const response = await zai.audio.tts.create({ ... });
|
||||
const buffer = Buffer.from(response.audio); // response.audio is undefined
|
||||
```
|
||||
|
||||
### Available Voices
|
||||
|
||||
- `tongtong` - 温暖亲切
|
||||
- `chuichui` - 活泼可爱
|
||||
- `xiaochen` - 沉稳专业
|
||||
- `jam` - 英音绅士
|
||||
- `kazi` - 清晰标准
|
||||
- `douji` - 自然流畅
|
||||
- `luodo` - 富有感染力
|
||||
|
||||
### Speed Range
|
||||
|
||||
- Minimum: `0.5` (half speed)
|
||||
- Default: `1.0` (normal speed)
|
||||
- Maximum: `2.0` (double speed)
|
||||
|
||||
**Important**: Speed values outside the range [0.5, 2.0] will result in API errors.
|
||||
|
||||
### Volume Range
|
||||
|
||||
- Minimum: Greater than `0` (exclusive)
|
||||
- Default: `1.0` (normal volume)
|
||||
- Maximum: `10` (inclusive)
|
||||
|
||||
**Note**: Volume parameter is optional. When not specified, defaults to 1.0.
|
||||
|
||||
## Remember
|
||||
|
||||
- Always use z-ai-web-dev-sdk in backend code only
|
||||
- **Input text is limited to 1024 characters maximum** - split longer text into chunks
|
||||
- **Speed must be between 0.5 and 2.0** - values outside this range will cause errors
|
||||
- **Volume must be greater than 0 and up to 10** - optional parameter with default 1.0
|
||||
- **Streaming only supports PCM format** - use non-streaming for WAV or MP3 output
|
||||
- The SDK returns a standard Response object - use `await response.arrayBuffer()`
|
||||
- Convert ArrayBuffer to Buffer using `Buffer.from(new Uint8Array(arrayBuffer))`
|
||||
- Handle audio buffers properly when saving to files
|
||||
- Implement error handling for production applications
|
||||
- Consider caching for frequently generated content
|
||||
- Clean up old audio files periodically to manage storage
|
||||
Executable
+25
@@ -0,0 +1,25 @@
|
||||
import ZAI from "z-ai-web-dev-sdk";
|
||||
import fs from "fs";
|
||||
|
||||
async function main(text: string, outFile: string) {
|
||||
try {
|
||||
const zai = await ZAI.create();
|
||||
|
||||
const response = await zai.audio.tts.create({
|
||||
input: text,
|
||||
voice: "tongtong",
|
||||
speed: 1.0,
|
||||
response_format: "wav",
|
||||
stream: false,
|
||||
});
|
||||
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
const buffer = Buffer.from(new Uint8Array(arrayBuffer));
|
||||
fs.writeFileSync(outFile, buffer);
|
||||
console.log(`TTS audio saved to ${outFile}`);
|
||||
} catch (err: any) {
|
||||
console.error("TTS failed:", err?.message || err);
|
||||
}
|
||||
}
|
||||
|
||||
main("Hello, world!", "./output.wav");
|
||||
Executable
+21
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 z-ai-web-dev-sdk Skills
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
Executable
+588
@@ -0,0 +1,588 @@
|
||||
---
|
||||
name: VLM
|
||||
description: Implement vision-based AI chat capabilities using the z-ai-web-dev-sdk. Use this skill when the user needs to analyze images, describe visual content, or create applications that combine image understanding with conversational AI. Supports image URLs and base64 encoded images for multimodal interactions.
|
||||
license: MIT
|
||||
---
|
||||
|
||||
# VLM(Vision Chat) Skill
|
||||
|
||||
This skill guides the implementation of vision chat functionality using the z-ai-web-dev-sdk package, enabling AI models to understand and respond to images combined with text prompts.
|
||||
|
||||
## Skills Path
|
||||
|
||||
**Skill Location**: `{project_path}/skills/VLM`
|
||||
|
||||
this skill is located at above path in your project.
|
||||
|
||||
**Reference Scripts**: Example test scripts are available in the `{Skill Location}/scripts/` directory for quick testing and reference. See `{Skill Location}/scripts/vlm.ts` for a working example.
|
||||
|
||||
## Overview
|
||||
|
||||
Vision Chat allows you to build applications that can analyze images, extract information from visual content, and answer questions about images through natural language conversation.
|
||||
|
||||
**IMPORTANT**: z-ai-web-dev-sdk MUST be used in backend code only. Never use it in client-side code.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
The z-ai-web-dev-sdk package is already installed. Import it as shown in the examples below.
|
||||
|
||||
## CLI Usage (For Simple Tasks)
|
||||
|
||||
For simple image analysis tasks, you can use the z-ai CLI instead of writing code. This is ideal for quick image descriptions, testing vision capabilities, or simple automation.
|
||||
|
||||
### Basic Image Analysis
|
||||
|
||||
```bash
|
||||
# Describe an image from URL
|
||||
z-ai vision --prompt "What's in this image?" --image "https://example.com/photo.jpg"
|
||||
|
||||
# Using short options
|
||||
z-ai vision -p "Describe this image" -i "https://example.com/image.png"
|
||||
```
|
||||
|
||||
### Analyze Local Images
|
||||
|
||||
```bash
|
||||
# Analyze a local image file
|
||||
z-ai vision -p "What objects are in this photo?" -i "./photo.jpg"
|
||||
|
||||
# Save response to file
|
||||
z-ai vision -p "Describe the scene" -i "./landscape.png" -o description.json
|
||||
```
|
||||
|
||||
### Multiple Images
|
||||
|
||||
```bash
|
||||
# Analyze multiple images at once
|
||||
z-ai vision \
|
||||
-p "Compare these two images" \
|
||||
-i "./photo1.jpg" \
|
||||
-i "./photo2.jpg" \
|
||||
-o comparison.json
|
||||
|
||||
# Multiple images with detailed analysis
|
||||
z-ai vision \
|
||||
--prompt "What are the differences between these images?" \
|
||||
--image "https://example.com/before.jpg" \
|
||||
--image "https://example.com/after.jpg"
|
||||
```
|
||||
|
||||
### With Thinking (Chain of Thought)
|
||||
|
||||
```bash
|
||||
# Enable thinking for complex visual reasoning
|
||||
z-ai vision \
|
||||
-p "Count the number of people in this image and describe their activities" \
|
||||
-i "./crowd.jpg" \
|
||||
--thinking \
|
||||
-o analysis.json
|
||||
```
|
||||
|
||||
### Streaming Output
|
||||
|
||||
```bash
|
||||
# Stream the vision analysis
|
||||
z-ai vision -p "Describe this image in detail" -i "./photo.jpg" --stream
|
||||
```
|
||||
|
||||
### CLI Parameters
|
||||
|
||||
- `--prompt, -p <text>`: **Required** - Question or instruction about the image(s)
|
||||
- `--image, -i <URL or path>`: Optional - Image URL or local file path (can be used multiple times)
|
||||
- `--thinking, -t`: Optional - Enable chain-of-thought reasoning (default: disabled)
|
||||
- `--output, -o <path>`: Optional - Output file path (JSON format)
|
||||
- `--stream`: Optional - Stream the response in real-time
|
||||
|
||||
### Supported Image Formats
|
||||
|
||||
- PNG (.png)
|
||||
- JPEG (.jpg, .jpeg)
|
||||
- GIF (.gif)
|
||||
- WebP (.webp)
|
||||
- BMP (.bmp)
|
||||
|
||||
### When to Use CLI vs SDK
|
||||
|
||||
**Use CLI for:**
|
||||
- Quick image analysis
|
||||
- Testing vision model capabilities
|
||||
- One-off image descriptions
|
||||
- Simple automation scripts
|
||||
|
||||
**Use SDK for:**
|
||||
- Multi-turn conversations with images
|
||||
- Dynamic image analysis in applications
|
||||
- Batch processing with custom logic
|
||||
- Production applications with complex workflows
|
||||
|
||||
## Recommended Approach
|
||||
|
||||
For better performance and reliability, use base64 encoding to pass images to the model instead of image URLs.
|
||||
|
||||
## Supported Content Types
|
||||
|
||||
The Vision Chat API supports three types of media content:
|
||||
|
||||
### 1. **image_url** - For Image Files
|
||||
Use this type for static images (PNG, JPEG, GIF, WebP, etc.)
|
||||
```typescript
|
||||
{
|
||||
role: 'user',
|
||||
content: [
|
||||
{ type: 'text', text: prompt },
|
||||
{ type: 'image_url', image_url: { url: imageUrl } }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 2. **video_url** - For Video Files
|
||||
Use this type for video content (MP4, AVI, MOV, etc.)
|
||||
```typescript
|
||||
{
|
||||
role: 'user',
|
||||
content: [
|
||||
{ type: 'text', text: prompt },
|
||||
{ type: 'video_url', video_url: { url: videoUrl } }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 3. **file_url** - For Document Files
|
||||
Use this type for document files (PDF, DOCX, TXT, etc.)
|
||||
```typescript
|
||||
{
|
||||
role: 'user',
|
||||
content: [
|
||||
{ type: 'text', text: prompt },
|
||||
{ type: 'file_url', file_url: { url: fileUrl } }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Note**: You can combine multiple content types in a single message. For example, you can include both text and multiple images, or text with both an image and a document.
|
||||
|
||||
## Basic Vision Chat Implementation
|
||||
|
||||
### Single Image Analysis
|
||||
|
||||
```javascript
|
||||
import ZAI from 'z-ai-web-dev-sdk';
|
||||
|
||||
async function analyzeImage(imageUrl, question) {
|
||||
const zai = await ZAI.create();
|
||||
|
||||
const response = await zai.chat.completions.createVision({
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: question
|
||||
},
|
||||
{
|
||||
type: 'image_url',
|
||||
image_url: {
|
||||
url: imageUrl
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
thinking: { type: 'disabled' }
|
||||
});
|
||||
|
||||
return response.choices[0]?.message?.content;
|
||||
}
|
||||
|
||||
// Usage
|
||||
const result = await analyzeImage(
|
||||
'https://example.com/product.jpg',
|
||||
'Describe this product in detail'
|
||||
);
|
||||
console.log('Analysis:', result);
|
||||
```
|
||||
|
||||
### Multiple Images Analysis
|
||||
|
||||
```javascript
|
||||
import ZAI from 'z-ai-web-dev-sdk';
|
||||
|
||||
async function compareImages(imageUrls, question) {
|
||||
const zai = await ZAI.create();
|
||||
|
||||
const content = [
|
||||
{
|
||||
type: 'text',
|
||||
text: question
|
||||
},
|
||||
...imageUrls.map(url => ({
|
||||
type: 'image_url',
|
||||
image_url: { url }
|
||||
}))
|
||||
];
|
||||
|
||||
const response = await zai.chat.completions.createVision({
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: content
|
||||
}
|
||||
],
|
||||
thinking: { type: 'disabled' }
|
||||
});
|
||||
|
||||
return response.choices[0]?.message?.content;
|
||||
}
|
||||
|
||||
// Usage
|
||||
const comparison = await compareImages(
|
||||
[
|
||||
'https://example.com/before.jpg',
|
||||
'https://example.com/after.jpg'
|
||||
],
|
||||
'Compare these two images and describe the differences'
|
||||
);
|
||||
```
|
||||
|
||||
### Base64 Image Support
|
||||
|
||||
```javascript
|
||||
import ZAI from 'z-ai-web-dev-sdk';
|
||||
import fs from 'fs';
|
||||
|
||||
async function analyzeLocalImage(imagePath, question) {
|
||||
const zai = await ZAI.create();
|
||||
|
||||
// Read image file and convert to base64
|
||||
const imageBuffer = fs.readFileSync(imagePath);
|
||||
const base64Image = imageBuffer.toString('base64');
|
||||
const mimeType = imagePath.endsWith('.png') ? 'image/png' : 'image/jpeg';
|
||||
|
||||
const response = await zai.chat.completions.createVision({
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: question
|
||||
},
|
||||
{
|
||||
type: 'image_url',
|
||||
image_url: {
|
||||
url: `data:${mimeType};base64,${base64Image}`
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
thinking: { type: 'disabled' }
|
||||
});
|
||||
|
||||
return response.choices[0]?.message?.content;
|
||||
}
|
||||
```
|
||||
|
||||
## Advanced Use Cases
|
||||
|
||||
### Conversational Vision Chat
|
||||
|
||||
```javascript
|
||||
import ZAI from 'z-ai-web-dev-sdk';
|
||||
|
||||
class VisionChatSession {
|
||||
constructor() {
|
||||
this.messages = [];
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
this.zai = await ZAI.create();
|
||||
}
|
||||
|
||||
async addImage(imageUrl, initialQuestion) {
|
||||
this.messages.push({
|
||||
role: 'user',
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: initialQuestion
|
||||
},
|
||||
{
|
||||
type: 'image_url',
|
||||
image_url: { url: imageUrl }
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
return this.getResponse();
|
||||
}
|
||||
|
||||
async followUp(question) {
|
||||
this.messages.push({
|
||||
role: 'user',
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: question
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
return this.getResponse();
|
||||
}
|
||||
|
||||
async getResponse() {
|
||||
const response = await this.zai.chat.completions.createVision({
|
||||
messages: this.messages,
|
||||
thinking: { type: 'disabled' }
|
||||
});
|
||||
|
||||
const assistantMessage = response.choices[0]?.message?.content;
|
||||
|
||||
this.messages.push({
|
||||
role: 'assistant',
|
||||
content: assistantMessage
|
||||
});
|
||||
|
||||
return assistantMessage;
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
const session = new VisionChatSession();
|
||||
await session.initialize();
|
||||
|
||||
const initial = await session.addImage(
|
||||
'https://example.com/chart.jpg',
|
||||
'What does this chart show?'
|
||||
);
|
||||
console.log('Initial analysis:', initial);
|
||||
|
||||
const followup = await session.followUp('What are the key trends?');
|
||||
console.log('Follow-up:', followup);
|
||||
```
|
||||
|
||||
### Image Classification and Tagging
|
||||
|
||||
```javascript
|
||||
import ZAI from 'z-ai-web-dev-sdk';
|
||||
|
||||
async function classifyImage(imageUrl) {
|
||||
const zai = await ZAI.create();
|
||||
|
||||
const prompt = `Analyze this image and provide:
|
||||
1. Main subject/category
|
||||
2. Key objects detected
|
||||
3. Scene description
|
||||
4. Suggested tags (comma-separated)
|
||||
|
||||
Format your response as JSON.`;
|
||||
|
||||
const response = await zai.chat.completions.createVision({
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: prompt
|
||||
},
|
||||
{
|
||||
type: 'image_url',
|
||||
image_url: { url: imageUrl }
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
thinking: { type: 'disabled' }
|
||||
});
|
||||
|
||||
const content = response.choices[0]?.message?.content;
|
||||
|
||||
try {
|
||||
return JSON.parse(content);
|
||||
} catch (e) {
|
||||
return { rawResponse: content };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### OCR and Text Extraction
|
||||
|
||||
```javascript
|
||||
import ZAI from 'z-ai-web-dev-sdk';
|
||||
|
||||
async function extractText(imageUrl) {
|
||||
const zai = await ZAI.create();
|
||||
|
||||
const response = await zai.chat.completions.createVision({
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: 'Extract all text from this image. Preserve the layout and formatting as much as possible.'
|
||||
},
|
||||
{
|
||||
type: 'image_url',
|
||||
image_url: { url: imageUrl }
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
thinking: { type: 'disabled' }
|
||||
});
|
||||
|
||||
return response.choices[0]?.message?.content;
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Image Quality and Size
|
||||
- Use high-quality images for better analysis results
|
||||
- Optimize image size to balance quality and processing speed
|
||||
- Supported formats: JPEG, PNG, WebP
|
||||
|
||||
### 2. Prompt Engineering
|
||||
- Be specific about what information you need from the image
|
||||
- Structure complex requests with numbered lists or bullet points
|
||||
- Provide context about the image type (photo, diagram, chart, etc.)
|
||||
|
||||
### 3. Error Handling
|
||||
```javascript
|
||||
async function safeVisionChat(imageUrl, question) {
|
||||
try {
|
||||
const zai = await ZAI.create();
|
||||
|
||||
const response = await zai.chat.completions.createVision({
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: [
|
||||
{ type: 'text', text: question },
|
||||
{ type: 'image_url', image_url: { url: imageUrl } }
|
||||
]
|
||||
}
|
||||
],
|
||||
thinking: { type: 'disabled' }
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
content: response.choices[0]?.message?.content
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Vision chat error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Performance Optimization
|
||||
- Cache SDK instance creation when processing multiple images
|
||||
- Use appropriate image formats (JPEG for photos, PNG for diagrams)
|
||||
- Consider image preprocessing for large batches
|
||||
|
||||
### 5. Security Considerations
|
||||
- Validate image URLs before processing
|
||||
- Sanitize user-provided image data
|
||||
- Implement rate limiting for public-facing APIs
|
||||
- Never expose SDK credentials in client-side code
|
||||
|
||||
## Common Use Cases
|
||||
|
||||
1. **Product Analysis**: Analyze product images for e-commerce applications
|
||||
2. **Document Understanding**: Extract information from receipts, invoices, forms
|
||||
3. **Medical Imaging**: Assist in preliminary analysis (with appropriate disclaimers)
|
||||
4. **Quality Control**: Detect defects or anomalies in manufacturing
|
||||
5. **Content Moderation**: Analyze images for policy compliance
|
||||
6. **Accessibility**: Generate alt text for images automatically
|
||||
7. **Visual Search**: Understand and categorize images for search functionality
|
||||
|
||||
## Integration Examples
|
||||
|
||||
### Express.js API Endpoint
|
||||
|
||||
```javascript
|
||||
import express from 'express';
|
||||
import ZAI from 'z-ai-web-dev-sdk';
|
||||
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
|
||||
let zaiInstance;
|
||||
|
||||
// Initialize SDK once
|
||||
async function initZAI() {
|
||||
zaiInstance = await ZAI.create();
|
||||
}
|
||||
|
||||
app.post('/api/analyze-image', async (req, res) => {
|
||||
try {
|
||||
const { imageUrl, question } = req.body;
|
||||
|
||||
if (!imageUrl || !question) {
|
||||
return res.status(400).json({
|
||||
error: 'imageUrl and question are required'
|
||||
});
|
||||
}
|
||||
|
||||
const response = await zaiInstance.chat.completions.createVision({
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: [
|
||||
{ type: 'text', text: question },
|
||||
{ type: 'image_url', image_url: { url: imageUrl } }
|
||||
]
|
||||
}
|
||||
],
|
||||
thinking: { type: 'disabled' }
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
analysis: response.choices[0]?.message?.content
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
initZAI().then(() => {
|
||||
app.listen(3000, () => {
|
||||
console.log('Vision chat API running on port 3000');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Issue**: "SDK must be used in backend"
|
||||
- **Solution**: Ensure z-ai-web-dev-sdk is only imported and used in server-side code
|
||||
|
||||
**Issue**: Image not loading or being analyzed
|
||||
- **Solution**: Verify the image URL is accessible and returns a valid image format
|
||||
|
||||
**Issue**: Poor analysis quality
|
||||
- **Solution**: Provide more specific prompts and ensure image quality is sufficient
|
||||
|
||||
**Issue**: Slow response times
|
||||
- **Solution**: Optimize image size and consider caching frequently analyzed images
|
||||
|
||||
## Remember
|
||||
|
||||
- Always use z-ai-web-dev-sdk in backend code only
|
||||
- The SDK is already installed - import as shown in examples
|
||||
- Structure prompts clearly for best results
|
||||
- Handle errors gracefully in production applications
|
||||
- Consider user privacy when processing images
|
||||
Executable
+57
@@ -0,0 +1,57 @@
|
||||
import ZAI, { VisionMessage } from 'z-ai-web-dev-sdk';
|
||||
|
||||
async function main(imageUrl: string, prompt: string) {
|
||||
try {
|
||||
const zai = await ZAI.create();
|
||||
|
||||
const messages: VisionMessage[] = [
|
||||
{
|
||||
role: 'assistant',
|
||||
content: [
|
||||
{ type: 'text', text: 'Output only text, no markdown.' }
|
||||
]
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: [
|
||||
{ type: 'text', text: prompt },
|
||||
{ type: 'image_url', image_url: { url: imageUrl } }
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
// const messages: VisionMessage[] = [
|
||||
// {
|
||||
// role: 'user',
|
||||
// content: [
|
||||
// { type: 'text', text: prompt },
|
||||
// { type: 'video_url', video_url: { url: imageUrl } }
|
||||
// ]
|
||||
// }
|
||||
// ];
|
||||
|
||||
// const messages: VisionMessage[] = [
|
||||
// {
|
||||
// role: 'user',
|
||||
// content: [
|
||||
// { type: 'text', text: prompt },
|
||||
// { type: 'file_url', file_url: { url: imageUrl } }
|
||||
// ]
|
||||
// }
|
||||
// ];
|
||||
|
||||
const response = await zai.chat.completions.createVision({
|
||||
model: 'glm-4.6v',
|
||||
messages,
|
||||
thinking: { type: 'disabled' }
|
||||
});
|
||||
|
||||
const reply = response.choices?.[0]?.message?.content;
|
||||
console.log('Vision model reply:');
|
||||
console.log(reply ?? JSON.stringify(response, null, 2));
|
||||
} catch (err: any) {
|
||||
console.error('Vision chat failed:', err?.message || err);
|
||||
}
|
||||
}
|
||||
|
||||
main("https://cdn.bigmodel.cn/static/logo/register.png", "Please describe this image.");
|
||||
Executable
+85
@@ -0,0 +1,85 @@
|
||||
# Changelog
|
||||
|
||||
## [Added Comment Feature - python-docx Method] - 2026-01-29
|
||||
|
||||
### Added
|
||||
- **批注功能 (Comment Feature)**: 使用python-docx的简单可靠方案
|
||||
- **推荐方法**: `scripts/add_comment_simple.py` - 使用python-docx直接操作.docx文件
|
||||
- **完整示例**: `scripts/examples/add_comments_pythondocx.py` - 展示各种使用场景
|
||||
- SKILL.md: 更新为推荐python-docx方法
|
||||
- ooxml.md: 保留OOXML方法作为高级选项
|
||||
- COMMENTS_UPDATE.md: 详细的功能更新说明
|
||||
|
||||
### Features
|
||||
- ✅ 简单易用:无需解压/打包文档
|
||||
- ✅ 批注人自动设置为"Z.ai"
|
||||
- ✅ 经过实际验证:在Word中正常显示
|
||||
- ✅ 支持多种定位方式:文本搜索、段落索引、条件判断等
|
||||
- ✅ 代码简洁:比OOXML方法简单得多
|
||||
|
||||
### Method Comparison
|
||||
|
||||
**Recommended: python-docx**
|
||||
```python
|
||||
from docx import Document
|
||||
doc = Document('input.docx')
|
||||
doc.add_comment(runs=[para.runs[0]], text="批注", author="Z.ai")
|
||||
doc.save('output.docx')
|
||||
```
|
||||
|
||||
**Alternative: OOXML (Advanced)**
|
||||
```python
|
||||
from scripts.document import Document
|
||||
doc = Document('unpacked', author="Z.ai")
|
||||
para = doc["word/document.xml"].get_node(tag="w:p", contains="text")
|
||||
doc.add_comment(start=para, end=para, text="批注")
|
||||
doc.save()
|
||||
```
|
||||
|
||||
### Usage Examples
|
||||
|
||||
#### 推荐方法(python-docx)
|
||||
```bash
|
||||
# 安装依赖
|
||||
pip install python-docx
|
||||
|
||||
# 使用简单脚本
|
||||
python scripts/add_comment_simple.py input.docx output.docx
|
||||
|
||||
# 使用完整示例
|
||||
python scripts/examples/add_comments_pythondocx.py document.docx reviewed.docx
|
||||
```
|
||||
|
||||
#### 高级方法(OOXML)
|
||||
```bash
|
||||
# 解压、处理、打包
|
||||
python ooxml/scripts/unpack.py document.docx unpacked
|
||||
python scripts/add_comment.py unpacked 10 "批注内容"
|
||||
python ooxml/scripts/pack.py unpacked output.docx
|
||||
```
|
||||
|
||||
### Testing
|
||||
- ✅ python-docx方法经过实际验证
|
||||
- ✅ 批注在Microsoft Word中正常显示
|
||||
- ✅ 作者正确显示为"Z.ai"
|
||||
- ✅ 支持各种定位方式
|
||||
- ✅ 代码简洁可靠
|
||||
|
||||
### Documentation
|
||||
- SKILL.md: 推荐python-docx方法,保留OOXML作为高级选项
|
||||
- COMMENTS_UPDATE.md: 详细说明两种方法的区别
|
||||
- 新增python-docx示例脚本
|
||||
- 保留OOXML示例供高级用户使用
|
||||
|
||||
### Why python-docx is Recommended
|
||||
1. **简单**: 无需解压/打包文档
|
||||
2. **可靠**: 经过实际验证,在Word中正常工作
|
||||
3. **直接**: 直接操作.docx文件,一步到位
|
||||
4. **维护性**: 代码简洁,易于理解和修改
|
||||
5. **兼容性**: 使用标准库,兼容性好
|
||||
|
||||
OOXML方法适合:
|
||||
- 需要低级XML控制
|
||||
- 需要同时处理tracked changes
|
||||
- 需要批注回复等复杂功能
|
||||
- 已经在使用解压文档的工作流
|
||||
Executable
+30
@@ -0,0 +1,30 @@
|
||||
© 2025 Anthropic, PBC. All rights reserved.
|
||||
|
||||
LICENSE: Use of these materials (including all code, prompts, assets, files,
|
||||
and other components of this Skill) is governed by your agreement with
|
||||
Anthropic regarding use of Anthropic's services. If no separate agreement
|
||||
exists, use is governed by Anthropic's Consumer Terms of Service or
|
||||
Commercial Terms of Service, as applicable:
|
||||
https://www.anthropic.com/legal/consumer-terms
|
||||
https://www.anthropic.com/legal/commercial-terms
|
||||
Your applicable agreement is referred to as the "Agreement." "Services" are
|
||||
as defined in the Agreement.
|
||||
|
||||
ADDITIONAL RESTRICTIONS: Notwithstanding anything in the Agreement to the
|
||||
contrary, users may not:
|
||||
|
||||
- Extract these materials from the Services or retain copies of these
|
||||
materials outside the Services
|
||||
- Reproduce or copy these materials, except for temporary copies created
|
||||
automatically during authorized use of the Services
|
||||
- Create derivative works based on these materials
|
||||
- Distribute, sublicense, or transfer these materials to any third party
|
||||
- Make, offer to sell, sell, or import any inventions embodied in these
|
||||
materials
|
||||
- Reverse engineer, decompile, or disassemble these materials
|
||||
|
||||
The receipt, viewing, or possession of these materials does not convey or
|
||||
imply any license or right beyond those expressly granted above.
|
||||
|
||||
Anthropic retains all right, title, and interest in these materials,
|
||||
including all copyrights, patents, and other intellectual property rights.
|
||||
Executable
+455
@@ -0,0 +1,455 @@
|
||||
---
|
||||
name: docx
|
||||
description: "Comprehensive document creation, editing, and analysis with support for tracked changes, comments, formatting preservation, and text extraction. When GLM needs to work with professional documents (.docx files) for: (1) Creating new documents, (2) Modifying or editing content, (3) Working with tracked changes, (4) Adding comments, or any other document tasks"
|
||||
license: Proprietary. LICENSE.txt has complete terms
|
||||
---
|
||||
|
||||
# DOCX creation, editing, and analysis
|
||||
|
||||
## Overview
|
||||
|
||||
A user may ask you to create, edit, or analyze the contents of a .docx file. A .docx file is essentially a ZIP archive containing XML files and other resources that you can read or edit. You have different tools and workflows available for different tasks.
|
||||
|
||||
# Design requiremnet
|
||||
|
||||
Deliver studio-quality Word documents with deep thought on content, functionality, and styling. Users often don't explicitly request advanced features (covers, TOC, backgrounds, back covers, footnotes, charts)—deeply understand needs and proactively extend. The document must have 1.3x line spacing and have charts centered horizontally.
|
||||
## Available color(choose one)
|
||||
- "Ink & Zen" Color Palette (Wabi-Sabi Style)
|
||||
The design uses a grayscale "Ink" palette to differentiate from standard business blue/morandi styles.
|
||||
Primary (Titles):#0B1220
|
||||
Body Text:#0F172A
|
||||
Secondary (Subtitles):#2B2B2B
|
||||
Accent (UI / Decor):#9AA6B2
|
||||
Table Header / Subtle Background:#F1F5F9
|
||||
|
||||
- Wilderness Oasis": Sage & Deep Forest
|
||||
Primary (Titles): #1A1F16 (Deep Forest Ink)
|
||||
Body Text: #2D3329 (Dark Moss Gray)
|
||||
Secondary (Subtitles): #4A5548 (Neutral Olive)
|
||||
Accent (UI/Decor): #94A3B8 (Steady Silver)
|
||||
Table/Background: #F8FAF7 (Ultra-Pale Mint White)
|
||||
|
||||
- "Terra Cotta Afterglow": Warm Clay & Greige
|
||||
Commonly utilized by top-tier consulting firms and architectural studios, this scheme warms up the gray scale to create a tactile sensation similar to premium cashmere.
|
||||
Primary (Titles): #26211F (Deep Charcoal Espresso)
|
||||
Body Text: #3D3735 (Dark Umber Gray)
|
||||
Secondary (Subtitles): #6B6361 (Warm Greige)
|
||||
Accent (UI/Decor): #C19A6B (Terra Cotta Gold / Muted Ochre)
|
||||
Table/Background: #FDFCFB (Off-White / Paper Texture)
|
||||
|
||||
- "Midnight Code": High-Contrast Slate & Silver
|
||||
Ideal for cutting-edge technology, AI ventures, or digital transformation projects. This palette carries a slight "electric" undertone that provides superior visual penetration.
|
||||
Primary (Titles): #020617 (Midnight Black)
|
||||
Body Text: #1E293B (Deep Slate Blue)
|
||||
Secondary (Subtitles): #64748B (Cool Blue-Gray)
|
||||
Accent (UI/Decor): #94A3B8 (Steady Silver)
|
||||
Table/Background: #F8FAFC (Glacial Blue-White)
|
||||
|
||||
### Chinese plot PNG method**
|
||||
If using Python to generate PNGs containing Chinese characters, note that Matplotlib defaults to the DejaVu Sans font which lacks Chinese support; since the environment already has the SimHei font installed, you should set it as the default by configuring:
|
||||
|
||||
matplotlib.font_manager.fontManager.addfont('/usr/share/fonts/truetype/chinese/SimHei.ttf')
|
||||
plt.rcParams['font.sans-serif'] = ['SimHei']
|
||||
plt.rcParams['axes.unicode_minus'] = False
|
||||
|
||||
|
||||
|
||||
|
||||
## Specialized Element Styling
|
||||
- Table Borders: Use a "Single" line style with a size of 12 and the Primary Ink color. Internal vertical borders should be set to Nil (invisible) to create a clean, modern horizontal-only look.
|
||||
- **CRITICAL: Table Cell Margins** - ALL tables MUST set `margins` property at the Table level to prevent text from touching borders. This is mandatory for professional document quality.
|
||||
|
||||
### Alignment and Typography
|
||||
CJK body: justify + 2-char indent. English: left. Table numbers: right. Headings: no indent.
|
||||
For both languages, Must use a line spacing of 1.3x (250 twips). Do not use single line spacing !!!
|
||||
|
||||
### CRITICAL: Chinese Quotes in JavaScript/TypeScript Code
|
||||
**MANDATORY**: When writing JavaScript/TypeScript code for docx-js, ALL Chinese quotation marks (""", ''') inside strings MUST be escaped as Unicode escape sequences:
|
||||
- Left double quote "\u201c" (")
|
||||
- Right double quote "\u201d" (")
|
||||
- Left single quote "\u2018" (')
|
||||
- Right single quote "\u2019" (')
|
||||
|
||||
**Example - INCORRECT (will cause syntax error):**
|
||||
```javascript
|
||||
new TextRun({
|
||||
text: "他说"你好"" // ERROR: Chinese quotes break JS syntax
|
||||
})
|
||||
```
|
||||
|
||||
**Example - CORRECT:**
|
||||
```javascript
|
||||
new TextRun({
|
||||
text: "他说\u201c你好\u201d" // Correct: escaped Unicode
|
||||
})
|
||||
```
|
||||
|
||||
**Alternative - Use template literals:**
|
||||
```javascript
|
||||
new TextRun({
|
||||
text: `他说"你好"` // Also works: template literals allow Chinese quotes
|
||||
})
|
||||
```
|
||||
|
||||
## Workflow Decision Tree
|
||||
|
||||
### Reading/Analyzing Content
|
||||
Use "Text extraction" or "Raw XML access" sections below.
|
||||
|
||||
### Creating New Document
|
||||
Use "Creating a new Word document" workflow.
|
||||
|
||||
### Editing Existing Document
|
||||
- **Your own document + simple changes**
|
||||
Use "Basic OOXML editing" workflow
|
||||
|
||||
- **Someone else's document**
|
||||
Use **"Redlining workflow"** (recommended default)
|
||||
|
||||
- **Legal, academic, business, or government docs**
|
||||
Use **"Redlining workflow"** (required)
|
||||
|
||||
## Reading and analyzing content
|
||||
|
||||
**Note**: For .doc (legacy format), first convert with `libreoffice --convert-to docx file.doc`.
|
||||
|
||||
### Text extraction
|
||||
If you just need to read the text contents of a document, you should convert the document to markdown using pandoc. Pandoc provides excellent support for preserving document structure and can show tracked changes:
|
||||
|
||||
```bash
|
||||
# Convert document to markdown with tracked changes
|
||||
pandoc --track-changes=all path-to-file.docx -o output.md
|
||||
# Options: --track-changes=accept/reject/all
|
||||
```
|
||||
|
||||
### Raw XML access
|
||||
You need raw XML access for: comments, complex formatting, document structure, embedded media, and metadata. For any of these features, you'll need to unpack a document and read its raw XML contents.
|
||||
|
||||
#### Unpacking a file
|
||||
`python ooxml/scripts/unpack.py <office_file> <output_directory>`
|
||||
|
||||
#### Key file structures
|
||||
* `word/document.xml` - Main document contents
|
||||
* `word/comments.xml` - Comments referenced in document.xml
|
||||
* `word/media/` - Embedded images and media files
|
||||
* Tracked changes use `<w:ins>` (insertions) and `<w:del>` (deletions) tags
|
||||
|
||||
## Creating a new Word document
|
||||
|
||||
When creating a new Word document from scratch, use **docx-js**, but use bun instead of node to implement it. which allows you to create Word documents using JavaScript/TypeScript.
|
||||
|
||||
### Workflow
|
||||
1. **MANDATORY - READ ENTIRE FILE**: Read [`docx-js.md`](docx-js.md) (~560 lines) completely from start to finish. **NEVER set any range limits when reading this file.** Read the full file content for detailed syntax, critical formatting rules, and best practices before proceeding with document creation.
|
||||
2. Create a JavaScript/TypeScript file using Document, Paragraph, TextRun components (You can assume all dependencies are installed, but if not, refer to the dependencies section below)
|
||||
3. Export as .docx using Packer.toBuffer()
|
||||
|
||||
### TOC (Table of Contents)
|
||||
**If the document has more than three sections, generate a table of contents.**
|
||||
|
||||
**Implementation**: Use docx-js `TableOfContents` component to create a live TOC that auto-populates from document headings.
|
||||
|
||||
**CRITICAL**: For TOC to work correctly:
|
||||
- All document headings MUST use `HeadingLevel` (e.g., `HeadingLevel.HEADING_1`)
|
||||
- Do NOT add custom styles to heading paragraphs
|
||||
- Place TOC before the actual heading content so it can scan them
|
||||
|
||||
**Hint requirement**: A hint paragraph MUST be added immediately after the TOC component with these specifications:
|
||||
- **Position**: Immediately after the TOC component
|
||||
- **Alignment**: Center-aligned
|
||||
- **Color**: Gray (e.g., "999999")
|
||||
- **Font size**: 18 (9pt)
|
||||
- **Language**: Matches user conversation language
|
||||
- **Text content**: Inform the user to right-click the TOC and select "Update Field" to show correct page numbers
|
||||
|
||||
### TOC Placeholders (Required Post-Processing)
|
||||
|
||||
**REQUIRED**: After generating the DOCX file, you MUST add placeholder TOC entries that appear on first open (before the user updates the TOC). This prevents showing an empty TOC initially.
|
||||
|
||||
**Implementation**: Always run the `add_toc_placeholders.py` script after generating the DOCX file:
|
||||
|
||||
```bash
|
||||
python skills/docx/scripts/add_toc_placeholders.py document.docx \
|
||||
--entries '[{"level":1,"text":"Chapter 1 Overview","page":"1"},{"level":2,"text":"Section 1.1 Details","page":"1"}]'
|
||||
```
|
||||
|
||||
**Note**: The script supports up to 3 TOC levels for placeholder entries.
|
||||
|
||||
**Entry format**:
|
||||
- `level`: Heading level (1, 2, or 3)
|
||||
- `text`: The heading text
|
||||
- `page`: Estimated page number (will be corrected when TOC is updated)
|
||||
|
||||
**Auto-generating entries**:
|
||||
You can extract the actual headings from the document structure to generate accurate entries. Match the heading text and hierarchy from your document content.
|
||||
|
||||
**Benefits**:
|
||||
- Users see TOC content immediately on first open
|
||||
- Placeholders are automatically replaced when user updates the TOC
|
||||
- Improves perceived document quality and user experience
|
||||
|
||||
### Document Formatting Rules
|
||||
|
||||
**Page Break Restrictions**
|
||||
Page breaks are ONLY allowed in these specific locations:
|
||||
- Between cover page and table of contents (if TOC exists)
|
||||
- Between cover page and main content (if NO TOC exists)
|
||||
- Between table of contents and main content (if TOC exists)
|
||||
|
||||
**All content after the table of contents must flow continuously WITHOUT page breaks.**
|
||||
|
||||
**Text and Paragraph Rules**
|
||||
- Complete sentences before starting a new line — do not break sentences across lines
|
||||
- Use single, consistent style for each complete sentence
|
||||
- Only start a new paragraph when the current paragraph is logically complete
|
||||
|
||||
**List and Bullet Point Formatting**
|
||||
- Use left-aligned formatting (NOT justified alignment)
|
||||
- Insert a line break after each list item
|
||||
- Never place multiple items on the same line (justification stretches text)
|
||||
|
||||
## Editing an existing Word document
|
||||
|
||||
**Note**: For .doc (legacy format), first convert with `libreoffice --convert-to docx file.doc`.
|
||||
|
||||
When editing an existing Word document, use the **Document library** (a Python library for OOXML manipulation). The library automatically handles infrastructure setup and provides methods for document manipulation. For complex scenarios, you can access the underlying DOM directly through the library.
|
||||
|
||||
### Workflow
|
||||
1. **MANDATORY - READ ENTIRE FILE**: Read [`ooxml.md`](ooxml.md) (~600 lines) completely from start to finish. **NEVER set any range limits when reading this file.** Read the full file content for the Document library API and XML patterns for directly editing document files.
|
||||
2. Unpack the document: `python ooxml/scripts/unpack.py <office_file> <output_directory>`
|
||||
3. Create and run a Python script using the Document library (see "Document Library" section in ooxml.md)
|
||||
4. Pack the final document: `python ooxml/scripts/pack.py <input_directory> <office_file>`
|
||||
|
||||
The Document library provides both high-level methods for common operations and direct DOM access for complex scenarios.
|
||||
|
||||
## Adding Comments (批注)
|
||||
|
||||
Comments (批注) allow you to add annotations to documents without modifying the actual content. This is useful for review feedback, explanations, or questions about specific parts of a document.
|
||||
|
||||
### Recommended Method: Using python-docx (简单推荐)
|
||||
|
||||
The simplest and most reliable way to add comments is using the `python-docx` library:
|
||||
|
||||
```python
|
||||
from docx import Document
|
||||
|
||||
# Open the document
|
||||
doc = Document('input.docx')
|
||||
|
||||
# Find paragraphs and add comments
|
||||
for para in doc.paragraphs:
|
||||
if "关键词" in para.text: # Find paragraphs containing specific text
|
||||
doc.add_comment(
|
||||
runs=[para.runs[0]], # Specify the text to comment on
|
||||
text="批注内容",
|
||||
author="Z.ai" # Set comment author as Z.ai
|
||||
)
|
||||
|
||||
# Save the document
|
||||
doc.save('output.docx')
|
||||
```
|
||||
|
||||
**Key points:**
|
||||
- Install: `pip install python-docx` or `bun add python-docx`
|
||||
- Works directly on .docx files (no need to unpack/pack)
|
||||
- Simple API, reliable results
|
||||
- Comments appear in Word's comment pane with Z.ai as author
|
||||
|
||||
**Common patterns:**
|
||||
|
||||
```python
|
||||
from docx import Document
|
||||
|
||||
doc = Document('document.docx')
|
||||
|
||||
# Add comment to first paragraph
|
||||
if doc.paragraphs:
|
||||
first_para = doc.paragraphs[0]
|
||||
doc.add_comment(
|
||||
runs=[first_para.runs[0]] if first_para.runs else [],
|
||||
text="Review this introduction",
|
||||
author="Z.ai"
|
||||
)
|
||||
|
||||
# Add comment to specific paragraph by index
|
||||
target_para = doc.paragraphs[5] # 6th paragraph
|
||||
doc.add_comment(
|
||||
runs=[target_para.runs[0]],
|
||||
text="This section needs clarification",
|
||||
author="Z.ai"
|
||||
)
|
||||
|
||||
# Add comments based on text search
|
||||
for para in doc.paragraphs:
|
||||
if "important" in para.text.lower():
|
||||
doc.add_comment(
|
||||
runs=[para.runs[0]],
|
||||
text="Flagged for review",
|
||||
author="Z.ai"
|
||||
)
|
||||
|
||||
doc.save('output.docx')
|
||||
```
|
||||
|
||||
### Alternative Method: Using OOXML (Advanced)
|
||||
|
||||
For complex scenarios requiring low-level XML manipulation, you can use the OOXML workflow. This method is more complex but provides finer control.
|
||||
|
||||
**Note:** This method requires unpacking/packing documents and may encounter validation issues. Use python-docx unless you specifically need low-level XML control.
|
||||
|
||||
#### OOXML Workflow
|
||||
|
||||
1. **Unpack the document**: `python ooxml/scripts/unpack.py <file.docx> <output_dir>`
|
||||
|
||||
2. **Create and run a Python script**:
|
||||
|
||||
```python
|
||||
from scripts.document import Document
|
||||
|
||||
# Initialize with Z.ai as the author
|
||||
doc = Document('unpacked', author="Z.ai", initials="Z")
|
||||
|
||||
# Add comment on a paragraph
|
||||
para = doc["word/document.xml"].get_node(tag="w:p", contains="paragraph text")
|
||||
doc.add_comment(start=para, end=para, text="This needs clarification")
|
||||
|
||||
# Save changes
|
||||
doc.save()
|
||||
```
|
||||
|
||||
3. **Pack the document**: `python ooxml/scripts/pack.py <unpacked_dir> <output.docx>`
|
||||
|
||||
**When to use OOXML method:**
|
||||
- You need to work with tracked changes simultaneously
|
||||
- You need fine-grained control over XML structure
|
||||
- You're already working with unpacked documents
|
||||
- You need to manipulate comments in complex ways
|
||||
|
||||
**When to use python-docx method (recommended):**
|
||||
- Adding comments is your primary task
|
||||
- You want simple, reliable code
|
||||
- You're working with complete .docx files
|
||||
- You don't need low-level XML access
|
||||
|
||||
## Redlining workflow for document review
|
||||
|
||||
This workflow allows you to plan comprehensive tracked changes using markdown before implementing them in OOXML. **CRITICAL**: For complete tracked changes, you must implement ALL changes systematically.
|
||||
|
||||
**Batching Strategy**: Group related changes into batches of 3-10 changes. This makes debugging manageable while maintaining efficiency. Test each batch before moving to the next.
|
||||
|
||||
**Principle: Minimal, Precise Edits**
|
||||
When implementing tracked changes, only mark text that actually changes. Repeating unchanged text makes edits harder to review and appears unprofessional. Break replacements into: [unchanged text] + [deletion] + [insertion] + [unchanged text]. Preserve the original run's RSID for unchanged text by extracting the `<w:r>` element from the original and reusing it.
|
||||
|
||||
Example - Changing "30 days" to "60 days" in a sentence:
|
||||
```python
|
||||
# BAD - Replaces entire sentence
|
||||
'<w:del><w:r><w:delText>The term is 30 days.</w:delText></w:r></w:del><w:ins><w:r><w:t>The term is 60 days.</w:t></w:r></w:ins>'
|
||||
|
||||
# GOOD - Only marks what changed, preserves original <w:r> for unchanged text
|
||||
'<w:r w:rsidR="00AB12CD"><w:t>The term is </w:t></w:r><w:del><w:r><w:delText>30</w:delText></w:r></w:del><w:ins><w:r><w:t>60</w:t></w:r></w:ins><w:r w:rsidR="00AB12CD"><w:t> days.</w:t></w:r>'
|
||||
```
|
||||
|
||||
### Tracked changes workflow
|
||||
|
||||
1. **Get markdown representation**: Convert document to markdown with tracked changes preserved:
|
||||
```bash
|
||||
pandoc --track-changes=all path-to-file.docx -o current.md
|
||||
```
|
||||
|
||||
2. **Identify and group changes**: Review the document and identify ALL changes needed, organizing them into logical batches:
|
||||
|
||||
**Location methods** (for finding changes in XML):
|
||||
- Section/heading numbers (e.g., "Section 3.2", "Article IV")
|
||||
- Paragraph identifiers if numbered
|
||||
- Grep patterns with unique surrounding text
|
||||
- Document structure (e.g., "first paragraph", "signature block")
|
||||
- **DO NOT use markdown line numbers** - they don't map to XML structure
|
||||
|
||||
**Batch organization** (group 3-10 related changes per batch):
|
||||
- By section: "Batch 1: Section 2 amendments", "Batch 2: Section 5 updates"
|
||||
- By type: "Batch 1: Date corrections", "Batch 2: Party name changes"
|
||||
- By complexity: Start with simple text replacements, then tackle complex structural changes
|
||||
- Sequential: "Batch 1: Pages 1-3", "Batch 2: Pages 4-6"
|
||||
|
||||
3. **Read documentation and unpack**:
|
||||
- **MANDATORY - READ ENTIRE FILE**: Read [`ooxml.md`](ooxml.md) (~600 lines) completely from start to finish. **NEVER set any range limits when reading this file.** Pay special attention to the "Document Library" and "Tracked Change Patterns" sections.
|
||||
- **Unpack the document**: `python ooxml/scripts/unpack.py <file.docx> <dir>`
|
||||
- **Note the suggested RSID**: The unpack script will suggest an RSID to use for your tracked changes. Copy this RSID for use in step 4b.
|
||||
|
||||
4. **Implement changes in batches**: Group changes logically (by section, by type, or by proximity) and implement them together in a single script. This approach:
|
||||
- Makes debugging easier (smaller batch = easier to isolate errors)
|
||||
- Allows incremental progress
|
||||
- Maintains efficiency (batch size of 3-10 changes works well)
|
||||
|
||||
**Suggested batch groupings:**
|
||||
- By document section (e.g., "Section 3 changes", "Definitions", "Termination clause")
|
||||
- By change type (e.g., "Date changes", "Party name updates", "Legal term replacements")
|
||||
- By proximity (e.g., "Changes on pages 1-3", "Changes in first half of document")
|
||||
|
||||
For each batch of related changes:
|
||||
|
||||
**a. Map text to XML**: Grep for text in `word/document.xml` to verify how text is split across `<w:r>` elements.
|
||||
|
||||
**b. Create and run script**: Use `get_node` to find nodes, implement changes, then `doc.save()`. See **"Document Library"** section in ooxml.md for patterns.
|
||||
|
||||
**Note**: Always grep `word/document.xml` immediately before writing a script to get current line numbers and verify text content. Line numbers change after each script run.
|
||||
|
||||
5. **Pack the document**: After all batches are complete, convert the unpacked directory back to .docx:
|
||||
```bash
|
||||
python ooxml/scripts/pack.py unpacked reviewed-document.docx
|
||||
```
|
||||
|
||||
6. **Final verification**: Do a comprehensive check of the complete document:
|
||||
- Convert final document to markdown:
|
||||
```bash
|
||||
pandoc --track-changes=all reviewed-document.docx -o verification.md
|
||||
```
|
||||
- Verify ALL changes were applied correctly:
|
||||
```bash
|
||||
grep "original phrase" verification.md # Should NOT find it
|
||||
grep "replacement phrase" verification.md # Should find it
|
||||
```
|
||||
- Check that no unintended changes were introduced
|
||||
|
||||
|
||||
## Converting Documents to Images
|
||||
|
||||
To visually analyze Word documents, convert them to images using a two-step process:
|
||||
|
||||
1. **Convert DOCX to PDF**:
|
||||
```bash
|
||||
soffice --headless --convert-to pdf document.docx
|
||||
```
|
||||
|
||||
2. **Convert PDF pages to JPEG images**:
|
||||
```bash
|
||||
pdftoppm -jpeg -r 150 document.pdf page
|
||||
```
|
||||
This creates files like `page-1.jpg`, `page-2.jpg`, etc.
|
||||
|
||||
Options:
|
||||
- `-r 150`: Sets resolution to 150 DPI (adjust for quality/size balance)
|
||||
- `-jpeg`: Output JPEG format (use `-png` for PNG if preferred)
|
||||
- `-f N`: First page to convert (e.g., `-f 2` starts from page 2)
|
||||
- `-l N`: Last page to convert (e.g., `-l 5` stops at page 5)
|
||||
- `page`: Prefix for output files
|
||||
|
||||
Example for specific range:
|
||||
```bash
|
||||
pdftoppm -jpeg -r 150 -f 2 -l 5 document.pdf page # Converts only pages 2-5
|
||||
```
|
||||
|
||||
## Code Style Guidelines
|
||||
**IMPORTANT**: When generating code for DOCX operations:
|
||||
- Write concise code
|
||||
- Avoid verbose variable names and redundant operations
|
||||
- Avoid unnecessary print statements
|
||||
|
||||
## Dependencies
|
||||
|
||||
Required dependencies (install if not available):
|
||||
|
||||
- **pandoc**: `sudo apt-get install pandoc` (for text extraction)
|
||||
- **docx**: `bun add docx` (for creating new documents)
|
||||
- **LibreOffice**: `sudo apt-get install libreoffice` (for PDF conversion)
|
||||
- **Poppler**: `sudo apt-get install poppler-utils` (for pdftoppm to convert PDF to images)
|
||||
- **defusedxml**: `pip install defusedxml` (for secure XML parsing)
|
||||
Executable
+681
@@ -0,0 +1,681 @@
|
||||
# DOCX Library Tutorial
|
||||
|
||||
Generate .docx files with JavaScript/TypeScript.
|
||||
|
||||
**Important: Read this entire document before starting.** Critical formatting rules and common pitfalls are covered throughout - skipping sections may result in corrupted files or rendering issues.
|
||||
|
||||
## Setup
|
||||
Assumes docx is already installed globally
|
||||
If not installed: first try `bun add docx`, then `npm install -g docx`
|
||||
```javascript
|
||||
const { Document, Packer, Paragraph, TextRun, Table, TableRow, TableCell, ImageRun, Media,
|
||||
Header, Footer, AlignmentType, PageOrientation, LevelFormat, ExternalHyperlink,
|
||||
InternalHyperlink, TableOfContents, HeadingLevel, BorderStyle, WidthType, TabStopType,
|
||||
TabStopPosition, UnderlineType, ShadingType, VerticalAlign, SymbolRun, PageNumber,
|
||||
FootnoteReferenceRun, Footnote, PageBreak } = require('docx');
|
||||
|
||||
// Create & Save
|
||||
const doc = new Document({ sections: [{ children: [/* content */] }] });
|
||||
Packer.toBuffer(doc).then(buffer => fs.writeFileSync("doc.docx", buffer)); // Node.js
|
||||
Packer.toBlob(doc).then(blob => { /* download logic */ }); // Browser
|
||||
```
|
||||
|
||||
## Delivery Standard
|
||||
|
||||
**Generic styling and mediocre aesthetics = mediocre delivery.**
|
||||
|
||||
Deliver studio-quality Word documents with deep thought on content, functionality, and styling. Users often don't explicitly request advanced features (covers, TOC, backgrounds, back covers, footnotes, charts)—deeply understand needs and proactively extend.
|
||||
|
||||
The following formatting standards are to be strictly applied without exception:
|
||||
|
||||
- Line Spacing: The entire document must use 1.3x line spacing.
|
||||
- Chart/Figure Placement: All charts, graphs, and figures must be explicitly centered horizontally on the page.
|
||||
|
||||
```javascript
|
||||
new Table({
|
||||
alignment: AlignmentType.CENTER,
|
||||
rows: [
|
||||
new TableRow({
|
||||
children: [
|
||||
new TableCell({
|
||||
children: [
|
||||
new Paragraph({
|
||||
text: "centered text",
|
||||
alignment: AlignmentType.CENTER,
|
||||
}),
|
||||
],
|
||||
verticalAlign: VerticalAlign.CENTER,
|
||||
shading: { fill: colors.tableBg },
|
||||
borders: cellBorders,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
- The text in charts must have left/right/up/bottom margin.
|
||||
- Image Handling:Preserve aspect ratio**: Never adjust image aspect ratio. Must insert according to the original ratio.
|
||||
- Do not use background shading to all table section headers.
|
||||
|
||||
Compliance with these specifications is mandatory.
|
||||
|
||||
## Language Consistency
|
||||
|
||||
**Document language = User conversation language** (including filename, body text, headings, headers, TOC hints, chart labels, and all other text).
|
||||
|
||||
## Headers and Footers - REQUIRED BY DEFAULT
|
||||
|
||||
Most documents **MUST** include headers and footers. The specific style (alignment, format, content) should match the document's overall design.
|
||||
|
||||
- **Header**: Typically document title, company name, or chapter name
|
||||
- **Footer**: Typically page numbers (format flexible: "X / Y", "Page X", "— X —", etc.)
|
||||
- **Cover/Back cover**: Use `TitlePage` setting to hide header/footer on first page
|
||||
|
||||
## Fonts
|
||||
If the user do not require specific fonts, you must follow the fonts rule belowing:
|
||||
### For Chinese:
|
||||
| Element | Font Family | Font Size (Half-points) | Properties |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| Normal Body | Microsoft YaHei (微软雅黑) | 21 (10.5pt / 五号) | Standard for readability. |
|
||||
| Heading 1 | SimHei (黑体) | 32 (16pt / 三号) | Bold, high impact. |
|
||||
| Heading 2 | SimHei (黑体) | 28 (14pt / 四号) | Bold. |
|
||||
| Caption | Microsoft YaHei | 20 (10pt) | For tables and charts. |
|
||||
|
||||
- Microsoft YaHei, located at /usr/share/fonts/truetype/chinese/msyh.ttf
|
||||
- SimHei, located at /usr/share/fonts/truetype/chinese/SimHei.ttf
|
||||
- Code blocks: SarasaMonoSC, located at /usr/share/fonts/truetype/chinese/SarasaMonoSC-Regular.ttf
|
||||
- Formulas / symbols: DejaVuSans, located at /usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf
|
||||
- For body text and formulas, use Paragraph instead of Preformatted.
|
||||
|
||||
|
||||
### For English
|
||||
| Element | Font Family | Font Size (Half-points) | Properties |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| Normal Body | Calibri | 22 (11pt) | Highly legible; slightly larger than 10.5pt to match visual "weight." |
|
||||
| Heading 1 | Times New Roman | 36 (18pt) | Bold, Serif; provides a clear "Newspaper" style hierarchy. |
|
||||
| Heading 2 | Times New Roman | 28 (14pt) | Bold; classic and professional. |
|
||||
| Caption | Calibri | 18 (9pt) | Clean and compact for metadata and notes. |
|
||||
|
||||
- Times New Roman, located at /usr/share/fonts/truetype/english/Times-New-Roman.ttf
|
||||
- Calibri,located at /usr/share/fonts/truetype/english/calibri-regular.ttf
|
||||
|
||||
## Spacing & Paragraph Alignment
|
||||
Task: Apply the following formatting rules to the provided text for a professional bilingual (Chinese/English) layout.
|
||||
### Paragraph & Indentation:
|
||||
Chinese Body: First-line indent of 2 characters (420 twips).
|
||||
English Body: No first-line indent; use block format (space between paragraphs).
|
||||
Alignment: Justified (Both) for all body text; Centered for Titles and Table Headers.
|
||||
### Line & Paragraph Spacing(keep in mind)
|
||||
Line Spacing: Set to 1.3 (250 twips) lines for both languages.
|
||||
Heading 1: 600 twips before, 300 twips after.
|
||||
### Mixed-Language Kerning:
|
||||
Insert a standard half-width space between Chinese characters and English words/numbers (e.g., "共 20 个 items").
|
||||
### Punctuation:
|
||||
Use full-width punctuation for Chinese text and half-width punctuation for English text.
|
||||
|
||||
## Professional Elements (Critical)
|
||||
|
||||
Produce documents that surpass user expectations by proactively incorporating high-end design elements without being prompted. Quality Benchmark: Visual excellence reflecting the standards of a top-tier designer in 2025.
|
||||
|
||||
**Cover & Visual:**
|
||||
- Double-Sided Branding: All formal documents (proposals, reports, contracts, bids) and creative assets (invitations, greeting cards) must include both a standalone front and back cover.
|
||||
- Internal Accents: Body pages may include subtle background elements to enhance the overall aesthetic depth.
|
||||
|
||||
**Structure:**
|
||||
- Navigation: For any document with three or more sections, include a Table of Contents (TOC) immediately followed by a "refresh hint."
|
||||
|
||||
**Data Presentation:**
|
||||
- Visual Priority: Use professional charts to illustrate trends or comparisons rather than plain text lists.
|
||||
- Table Aesthetics: Apply light gray headers or the "three-line" professional style; strictly avoid the default Word blue.
|
||||
|
||||
**Links & References:**
|
||||
- Interactive Links: All URLs must be formatted as clickable, active hyperlinks.
|
||||
- Cross-Referencing: Number all figures and tables systematically (e.g., "see Figure 1") and use internal cross-references.
|
||||
- Academic/Legal Rigor: For research or data-heavy documents, implement clickable in-text citations paired with accurate footnotes or endnotes.
|
||||
|
||||
### TOC Refresh Hint
|
||||
|
||||
Because Word TOCs utilize field codes, page numbers may become unaligned during generation. You must append the following gray hint text after the TOC to guide the user:
|
||||
Note: This Table of Contents is generated via field codes. To ensure page number accuracy after editing, please right-click the TOC and select "Update Field."
|
||||
|
||||
### Outline Adherence
|
||||
|
||||
- **User provides outline**: Follow strictly, no additions, deletions, or reordering
|
||||
- **No outline provided**: Use standard structure
|
||||
- Academic: Introduction → Literature Review → Methodology → Results → Discussion → Conclusion.
|
||||
- Business: Executive Summary → Analysis → Recommendations.
|
||||
- Technical: Overview → Principles → Implementation → Examples → FAQ.
|
||||
|
||||
### Scene Completeness
|
||||
|
||||
Anticipate the functional requirements of the specific scenario. Examples include, but are not limited to:
|
||||
- **Exam paper** → Include name/class/ID fields, point allocations for every question, and a dedicated grading table.
|
||||
- **Contract** → Provide signature and seal blocks for all parties, date placeholders, contract ID numbers, and an attachment list.
|
||||
- **Meeting minutes** → List attendees and absentees, define action items with assigned owners, and note the next meeting time.
|
||||
|
||||
## Design Philosophy
|
||||
|
||||
### Color Scheme
|
||||
|
||||
**Low saturation tones**, avoid Word default blue and matplotlib default high saturation.
|
||||
|
||||
**Flexibly choose** color schemes based on document scenario:
|
||||
|
||||
| Style | Palette | Suitable Scenarios |
|
||||
|-------|---------|-------------------|
|
||||
| Morandi | Soft muted tones | Arts, editorial, lifestyle |
|
||||
| Earth tones | Brown, olive, natural | Environmental, organic industries |
|
||||
| Nordic | Cool gray, misty blue | Minimalism, technology, software |
|
||||
| Japanese Wabi-sabi | Gray, raw wood, zen | Traditional, contemplative, crafts |
|
||||
| French elegance | Off-white, dusty pink | Luxury, fashion, high-end retail |
|
||||
| Industrial | Charcoal, rust, concrete | Manufacturing, engineering, construction |
|
||||
| Academic | Navy, burgundy, ivory | Research, education, legal |
|
||||
| Ocean mist | Misty blue, sand | Marine, wellness, travel |
|
||||
| Forest moss | Olive, moss green | Nature, sustainability, forestry |
|
||||
| Desert dusk | Ochre, sandy gold | Warmth, regional, historical |
|
||||
|
||||
**Color scheme must be consistent within the same document.**
|
||||
|
||||
### highlighting
|
||||
Use low saturation color schemes for font highlighting.
|
||||
|
||||
### Layout
|
||||
|
||||
White space (margins, paragraph spacing), clear hierarchy (H1 > H2 > body), proper padding (text shouldn't touch borders).
|
||||
|
||||
### Pagination Control
|
||||
|
||||
Word uses flow layout, not fixed pages.
|
||||
|
||||
### Alignment and Typography (keep in mind!!!)
|
||||
CJK body: justify + 2-char indent. English: left. Table numbers: right. Headings: no indent.
|
||||
For both languages, Must use a line spacing of 1.3x (250 twips). Do not use single line spacing !!!
|
||||
|
||||
### Table Formatting(Very inportant)
|
||||
- A caption must be added immediately after the table, keep in mind!
|
||||
- The entire table must be centered horizontally on the page. keep in mind!
|
||||
#### Cell Formatting (Inside the Table)
|
||||
Left/Right Cell Margin: Set to at least 120-200 twips (approximately the width of one character).
|
||||
Up/Down Cell Margin: Set to at least 100 twips
|
||||
Text Alignment(must follow !!!):
|
||||
- Horizontal Alignment: Center-aligned. This creates a clean vertical axis through the table column.
|
||||
- Vertical Alignment: Center-aligned. Text must be positioned exactly in the middle of the cell's height to prevent it from "floating" too close to the top or bottom borders.
|
||||
- Cell Margins (Padding):
|
||||
Left/Right: Set to 120–200 twips (approx. 0.2–0.35 cm). This ensures text does not touch the borders, maintaining legibility.
|
||||
Top/Bottom: Set to at least 60–100 twips to provide a consistent vertical buffer around the text.
|
||||
|
||||
|
||||
### Page break
|
||||
There must be page break between cover page and the content, between table of content and the content also, should NOT put cover page and content in a single page.
|
||||
|
||||
## Page Layout & Margins (A4 Standard)
|
||||
The layout uses a 1440 twip (1 inch) margin for content, with specialized margins for the cover.
|
||||
|
||||
| Section | Top Margin | Bottom/Left/Right | Twips Calculation |
|
||||
|---------------|------------|-------------------|-------------------------------------------|
|
||||
| Cover Page | 0 | 0 | For edge-to-edge background images. |
|
||||
| Main Content | 1800 | 1440 | Extra top space for the header. |
|
||||
| **Twips Unit** | **1 inch = 1440 twips** | **A4 Width = 11906** | **A4 Height = 16838** |
|
||||
|
||||
## Text & Formatting
|
||||
```javascript
|
||||
// IMPORTANT: Never use \n for line breaks - always use separate Paragraph elements
|
||||
// ❌ WRONG: new TextRun("Line 1\nLine 2")
|
||||
// ✅ CORRECT: new Paragraph({ children: [new TextRun("Line 1")] }), new Paragraph({ children: [new TextRun("Line 2")] })
|
||||
|
||||
// First-line indent for body paragraphs
|
||||
// IMPORTANT: Chinese documents typically use 2-character indent (about 480 DXA for 12pt SimSun)
|
||||
new Paragraph({
|
||||
indent: { firstLine: 480 }, // 2-character first-line indent for Chinese body text
|
||||
children: [new TextRun({ text: "This is the main text (Chinese). The first line is indented by two characters.", font: "SimSun" })]
|
||||
})
|
||||
|
||||
// Basic text with all formatting options
|
||||
new Paragraph({
|
||||
alignment: AlignmentType.CENTER,
|
||||
spacing: { before: 200, after: 200 },
|
||||
indent: { left: 720, right: 720, firstLine: 480 }, // Can combine with left/right indent
|
||||
children: [
|
||||
new TextRun({ text: "Bold", bold: true }),
|
||||
new TextRun({ text: "Italic", italics: true }),
|
||||
new TextRun({ text: "Underlined", underline: { type: UnderlineType.DOUBLE, color: "FF0000" } }),
|
||||
new TextRun({ text: "Colored", color: "FF0000", size: 28, font: "Times New Roman" }), // Times New Roman (system font)
|
||||
new TextRun({ text: "Highlighted", highlight: "yellow" }),
|
||||
new TextRun({ text: "Strikethrough", strike: true }),
|
||||
new TextRun({ text: "x2", superScript: true }),
|
||||
new TextRun({ text: "H2O", subScript: true }),
|
||||
new TextRun({ text: "SMALL CAPS", smallCaps: true }),
|
||||
new SymbolRun({ char: "2022", font: "Symbol" }), // Bullet •
|
||||
new SymbolRun({ char: "00A9", font: "Arial" }) // Copyright © - Arial for symbols
|
||||
]
|
||||
})
|
||||
```
|
||||
|
||||
## Styles & Professional Formatting
|
||||
|
||||
```javascript
|
||||
const doc = new Document({
|
||||
styles: {
|
||||
default: { document: { run: { font: "Times New Roman", size: 24 } } }, // 12pt default (system font)
|
||||
paragraphStyles: [
|
||||
// Document title style - override built-in Title style
|
||||
{ id: "Title", name: "Title", basedOn: "Normal",
|
||||
run: { size: 56, bold: true, color: "000000", font: "Times New Roman" },
|
||||
paragraph: { spacing: { before: 240, after: 120 }, alignment: AlignmentType.CENTER } },
|
||||
// IMPORTANT: Override built-in heading styles by using their exact IDs
|
||||
{ id: "Heading1", name: "Heading 1", basedOn: "Normal", next: "Normal", quickFormat: true,
|
||||
run: { size: 32, bold: true, color: "000000", font: "Times New Roman" }, // 16pt
|
||||
paragraph: { spacing: { before: 240, after: 240 }, outlineLevel: 0 } }, // outlineLevel enables TOC generation if needed
|
||||
{ id: "Heading2", name: "Heading 2", basedOn: "Normal", next: "Normal", quickFormat: true,
|
||||
run: { size: 28, bold: true, color: "000000", font: "Times New Roman" }, // 14pt
|
||||
paragraph: { spacing: { before: 180, after: 180 }, outlineLevel: 1 } },
|
||||
// Custom styles use your own IDs
|
||||
{ id: "myStyle", name: "My Style", basedOn: "Normal",
|
||||
run: { size: 28, bold: true, color: "000000" },
|
||||
paragraph: { spacing: { after: 120 }, alignment: AlignmentType.CENTER } }
|
||||
],
|
||||
characterStyles: [{ id: "myCharStyle", name: "My Char Style",
|
||||
run: { color: "FF0000", bold: true, underline: { type: UnderlineType.SINGLE } } }]
|
||||
},
|
||||
sections: [{
|
||||
properties: { page: { margin: { top: 1440, right: 1440, bottom: 1440, left: 1440 } } },
|
||||
children: [
|
||||
new Paragraph({ heading: HeadingLevel.TITLE, children: [new TextRun("Document Title")] }), // Uses overridden Title style
|
||||
new Paragraph({ heading: HeadingLevel.HEADING_1, children: [new TextRun("Heading 1")] }), // Uses overridden Heading1 style
|
||||
new Paragraph({ style: "myStyle", children: [new TextRun("Custom paragraph style")] }),
|
||||
new Paragraph({ children: [
|
||||
new TextRun("Normal with "),
|
||||
new TextRun({ text: "custom char style", style: "myCharStyle" })
|
||||
]})
|
||||
]
|
||||
}]
|
||||
});
|
||||
```
|
||||
|
||||
**Font Management Strategy (CRITICAL):**
|
||||
|
||||
**ALWAYS prioritize system-installed fonts** for reliability, performance, and cross-platform compatibility:
|
||||
|
||||
1. **System fonts FIRST** (no download, immediate availability):
|
||||
- English: **Times New Roman** (professional standard)
|
||||
- Chinese: **SimSun/宋体** (formal document standard)
|
||||
- Universal fallbacks: Arial, Calibri, Helvetica
|
||||
|
||||
2. **Avoid custom font downloads** unless absolutely necessary for specific branding
|
||||
3. **Test font availability** before deployment
|
||||
|
||||
**Professional Font Combinations (System Fonts Only):**
|
||||
- **Times New Roman (Headers) + Times New Roman (Body)** - Classic, professional, universally supported
|
||||
- **Arial (Headers) + Arial (Body)** - Clean, modern, universally supported
|
||||
- **Times New Roman (Headers) + Arial (Body)** - Classic serif headers with modern body
|
||||
|
||||
**Chinese Document Font Guidelines (System Fonts):**
|
||||
- **Body text**: Use **SimSun/宋体** - the standard system font for Chinese formal documents
|
||||
- **Headings**: Use **SimHei/黑体** - bold sans-serif for visual hierarchy
|
||||
- **Default size**: 12pt (size: 24) for body, 14-16pt for headings
|
||||
- **CRITICAL**: SimSun for body text, SimHei ONLY for headings - never use SimHei for entire document
|
||||
|
||||
```javascript
|
||||
// English document style configuration (Times New Roman)
|
||||
const doc = new Document({
|
||||
styles: {
|
||||
default: { document: { run: { font: "Times New Roman", size: 24 } } }, // 12pt for body
|
||||
paragraphStyles: [
|
||||
{ id: "Heading1", name: "Heading 1", basedOn: "Normal", next: "Normal", quickFormat: true,
|
||||
run: { size: 32, bold: true, font: "Times New Roman" }, // 16pt for H1
|
||||
paragraph: { spacing: { before: 240, after: 240 }, outlineLevel: 0 } },
|
||||
{ id: "Heading2", name: "Heading 2", basedOn: "Normal", next: "Normal", quickFormat: true,
|
||||
run: { size: 28, bold: true, font: "Times New Roman" }, // 14pt for H2
|
||||
paragraph: { spacing: { before: 180, after: 180 }, outlineLevel: 1 } }
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
// Chinese document style configuration (SimSun/SimHei)
|
||||
const doc = new Document({
|
||||
styles: {
|
||||
default: { document: { run: { font: "SimSun", size: 24 } } }, // SimSun 12pt for body
|
||||
paragraphStyles: [
|
||||
{ id: "Heading1", name: "Heading 1", basedOn: "Normal", next: "Normal", quickFormat: true,
|
||||
run: { size: 32, bold: true, font: "SimHei" }, // SimHei 16pt for H1
|
||||
paragraph: { spacing: { before: 240, after: 240 }, outlineLevel: 0 } },
|
||||
{ id: "Heading2", name: "Heading 2", basedOn: "Normal", next: "Normal", quickFormat: true,
|
||||
run: { size: 28, bold: true, font: "SimHei" }, // SimHei 14pt for H2
|
||||
paragraph: { spacing: { before: 180, after: 180 }, outlineLevel: 1 } }
|
||||
]
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
**Key Styling Principles:**
|
||||
- **ALWAYS use system-installed fonts** (Times New Roman for English, SimSun for Chinese)
|
||||
- **Override built-in styles**: Use exact IDs like "Heading1", "Heading2", "Heading3" to override Word's built-in heading styles
|
||||
- **HeadingLevel constants**: `HeadingLevel.HEADING_1` uses "Heading1" style, `HeadingLevel.HEADING_2` uses "Heading2" style, etc.
|
||||
- **outlineLevel**: Set `outlineLevel: 0` for H1, `outlineLevel: 1` for H2, etc. (optional, only needed if TOC will be added)
|
||||
- **Use custom styles** instead of inline formatting for consistency
|
||||
- **Set a default font** using `styles.default.document.run.font` - Times New Roman for English, SimSun for Chinese
|
||||
- **Establish visual hierarchy** with different font sizes (titles > headers > body)
|
||||
- **Add proper spacing** with `before` and `after` paragraph spacing
|
||||
- **Use colors sparingly**: Default to black (000000) and shades of gray for titles and headings (heading 1, heading 2, etc.)
|
||||
- **Set consistent margins** (1440 = 1 inch is standard)
|
||||
|
||||
|
||||
## Lists (ALWAYS USE PROPER LISTS - NEVER USE UNICODE BULLETS)
|
||||
|
||||
### ⚠️ CRITICAL: Numbered List References - Read This Before Creating Lists!
|
||||
|
||||
**Each independently numbered list MUST use a UNIQUE reference name**
|
||||
|
||||
**Rules**:
|
||||
- Same `reference` = continues numbering (1,2,3 → 4,5,6)
|
||||
- Different `reference` = restarts at 1 (1,2,3 → 1,2,3)
|
||||
|
||||
**When to use a new reference?**
|
||||
- ✓ Numbered lists under new headings/sections
|
||||
- ✓ Any list that needs independent numbering
|
||||
- ✗ Subsequent items of the same list (keep using same reference)
|
||||
|
||||
**Reference naming suggestions**:
|
||||
- `list-section-1`, `list-section-2`, `list-section-3`
|
||||
- `list-chapter-1`, `list-chapter-2`
|
||||
- `list-requirements`, `list-constraints` (name based on content)
|
||||
|
||||
```javascript
|
||||
// ❌ WRONG: All lists use the same reference
|
||||
numbering: {
|
||||
config: [
|
||||
{ reference: "my-list", levels: [...] } // Only one config
|
||||
]
|
||||
}
|
||||
// Result:
|
||||
// Chapter 1
|
||||
// 1. Item A
|
||||
// 2. Item B
|
||||
// Chapter 2
|
||||
// 3. Item C ← WRONG! Should start from 1
|
||||
// 4. Item D
|
||||
|
||||
// ✅ CORRECT: Each list uses different reference
|
||||
numbering: {
|
||||
config: [
|
||||
{ reference: "list-chapter-1", levels: [...] },
|
||||
{ reference: "list-chapter-2", levels: [...] },
|
||||
{ reference: "list-chapter-3", levels: [...] }
|
||||
]
|
||||
}
|
||||
// Result:
|
||||
// Chapter 1
|
||||
// 1. Item A
|
||||
// 2. Item B
|
||||
// Chapter 2
|
||||
// 1. Item C ✓ CORRECT! Restarts from 1
|
||||
// 2. Item D
|
||||
// Chapter 3
|
||||
// 1. Item E ✓ CORRECT! Restarts from 1
|
||||
// 2. Item F
|
||||
```
|
||||
|
||||
### Basic List Syntax
|
||||
|
||||
```javascript
|
||||
// Bullets - ALWAYS use the numbering config, NOT unicode symbols
|
||||
// CRITICAL: Use LevelFormat.BULLET constant, NOT the string "bullet"
|
||||
const doc = new Document({
|
||||
numbering: {
|
||||
config: [
|
||||
{ reference: "bullet-list",
|
||||
levels: [{ level: 0, format: LevelFormat.BULLET, text: "•", alignment: AlignmentType.LEFT,
|
||||
style: { paragraph: { indent: { left: 720, hanging: 360 } } } }] },
|
||||
{ reference: "first-numbered-list",
|
||||
levels: [{ level: 0, format: LevelFormat.DECIMAL, text: "%1.", alignment: AlignmentType.LEFT,
|
||||
style: { paragraph: { indent: { left: 720, hanging: 360 } } } }] },
|
||||
{ reference: "second-numbered-list", // Different reference = restarts at 1
|
||||
levels: [{ level: 0, format: LevelFormat.DECIMAL, text: "%1.", alignment: AlignmentType.LEFT,
|
||||
style: { paragraph: { indent: { left: 720, hanging: 360 } } } }] }
|
||||
]
|
||||
},
|
||||
sections: [{
|
||||
children: [
|
||||
// Bullet list items
|
||||
new Paragraph({ numbering: { reference: "bullet-list", level: 0 },
|
||||
children: [new TextRun("First bullet point")] }),
|
||||
new Paragraph({ numbering: { reference: "bullet-list", level: 0 },
|
||||
children: [new TextRun("Second bullet point")] }),
|
||||
// Numbered list items
|
||||
new Paragraph({ numbering: { reference: "first-numbered-list", level: 0 },
|
||||
children: [new TextRun("First numbered item")] }),
|
||||
new Paragraph({ numbering: { reference: "first-numbered-list", level: 0 },
|
||||
children: [new TextRun("Second numbered item")] }),
|
||||
// ⚠️ CRITICAL: Different reference = INDEPENDENT list that restarts at 1
|
||||
// Same reference = CONTINUES previous numbering
|
||||
new Paragraph({ numbering: { reference: "second-numbered-list", level: 0 },
|
||||
children: [new TextRun("Starts at 1 again (because different reference)")] })
|
||||
]
|
||||
}]
|
||||
});
|
||||
|
||||
// ⚠️ CRITICAL: NEVER use unicode bullets - they create fake lists that don't work properly
|
||||
// new TextRun("• Item") // WRONG
|
||||
// new SymbolRun({ char: "2022" }) // WRONG
|
||||
// ✅ ALWAYS use numbering config with LevelFormat.BULLET for real Word lists
|
||||
```
|
||||
|
||||
## Tables
|
||||
```javascript
|
||||
// Complete table with margins, borders, headers, and bullet points
|
||||
const tableBorder = { style: BorderStyle.SINGLE, size: 1, color: "CCCCCC" };
|
||||
const cellBorders = { top: tableBorder, bottom: tableBorder, left: tableBorder, right: tableBorder };
|
||||
|
||||
new Table({
|
||||
columnWidths: [4680, 4680], // ⚠️ CRITICAL: Set column widths at table level - values in DXA (twentieths of a point)
|
||||
// ⚠️ MANDATORY: margins MUST be set to prevent text touching borders
|
||||
margins: { top: 100, bottom: 100, left: 180, right: 180 }, // Minimum comfortable padding
|
||||
rows: [
|
||||
new TableRow({
|
||||
tableHeader: true,
|
||||
children: [
|
||||
new TableCell({
|
||||
borders: cellBorders,
|
||||
width: { size: 4680, type: WidthType.DXA }, // ALSO set width on each cell
|
||||
// ⚠️ CRITICAL: Always use ShadingType.CLEAR to prevent black backgrounds in Word.
|
||||
shading: { fill: "D5E8F0", type: ShadingType.CLEAR },
|
||||
verticalAlign: VerticalAlign.CENTER,
|
||||
children: [new Paragraph({
|
||||
alignment: AlignmentType.CENTER,
|
||||
children: [new TextRun({ text: "Header", bold: true, size: 22 })]
|
||||
})]
|
||||
}),
|
||||
new TableCell({
|
||||
borders: cellBorders,
|
||||
width: { size: 4680, type: WidthType.DXA }, // ALSO set width on each cell
|
||||
shading: { fill: "D5E8F0", type: ShadingType.CLEAR },
|
||||
children: [new Paragraph({
|
||||
alignment: AlignmentType.CENTER,
|
||||
children: [new TextRun({ text: "Bullet Points", bold: true, size: 22 })]
|
||||
})]
|
||||
})
|
||||
]
|
||||
}),
|
||||
new TableRow({
|
||||
children: [
|
||||
new TableCell({
|
||||
borders: cellBorders,
|
||||
width: { size: 4680, type: WidthType.DXA }, // ALSO set width on each cell
|
||||
children: [new Paragraph({ children: [new TextRun("Regular data")] })]
|
||||
}),
|
||||
new TableCell({
|
||||
borders: cellBorders,
|
||||
width: { size: 4680, type: WidthType.DXA }, // ALSO set width on each cell
|
||||
children: [
|
||||
new Paragraph({
|
||||
numbering: { reference: "bullet-list", level: 0 },
|
||||
children: [new TextRun("First bullet point")]
|
||||
}),
|
||||
new Paragraph({
|
||||
numbering: { reference: "bullet-list", level: 0 },
|
||||
children: [new TextRun("Second bullet point")]
|
||||
})
|
||||
]
|
||||
})
|
||||
]
|
||||
})
|
||||
]
|
||||
})
|
||||
```
|
||||
|
||||
**IMPORTANT: Table Width & Borders**
|
||||
- Use BOTH `columnWidths: [width1, width2, ...]` array AND `width: { size: X, type: WidthType.DXA }` on each cell
|
||||
- Values in DXA (twentieths of a point): 1440 = 1 inch, Letter usable width = 9360 DXA (with 1" margins)
|
||||
- Apply borders to individual `TableCell` elements, NOT the `Table` itself
|
||||
|
||||
**Precomputed Column Widths (Letter size with 1" margins = 9360 DXA total):**
|
||||
- **2 columns:** `columnWidths: [4680, 4680]` (equal width)
|
||||
- **3 columns:** `columnWidths: [3120, 3120, 3120]` (equal width)
|
||||
|
||||
## Links & Navigation
|
||||
```javascript
|
||||
// TOC example
|
||||
// new TableOfContents("Table of Contents", { hyperlink: true, headingStyleRange: "1-3" }),
|
||||
//
|
||||
// CRITICAL: If adding TOC, use HeadingLevel only, NOT custom styles
|
||||
// ❌ WRONG: new Paragraph({ heading: HeadingLevel.HEADING_1, style: "customHeader", children: [new TextRun("Title")] })
|
||||
// ✅ CORRECT: new Paragraph({ heading: HeadingLevel.HEADING_1, children: [new TextRun("Title")] })
|
||||
|
||||
// REQUIRED: After generating the DOCX, add TOC placeholders for first-open experience
|
||||
// Always run: python skills/docx/scripts/add_toc_placeholders.py document.docx --entries '[...]'
|
||||
// This adds placeholder entries that appear before the user updates the TOC (modifies file in-place)
|
||||
// Extract headings from your document to generate accurate entries
|
||||
|
||||
// External link
|
||||
new Paragraph({
|
||||
children: [new ExternalHyperlink({
|
||||
children: [new TextRun({ text: "Google", style: "Hyperlink" })],
|
||||
link: "https://www.google.com"
|
||||
})]
|
||||
}),
|
||||
|
||||
// Internal link & bookmark
|
||||
new Paragraph({
|
||||
children: [new InternalHyperlink({
|
||||
children: [new TextRun({ text: "Go to Section", style: "Hyperlink" })],
|
||||
anchor: "section1"
|
||||
})]
|
||||
}),
|
||||
new Paragraph({
|
||||
children: [new TextRun("Section Content")],
|
||||
bookmark: { id: "section1", name: "section1" }
|
||||
}),
|
||||
|
||||
```
|
||||
|
||||
Use `new Paragraph({ children: [new PageBreak()] })` at the start of the next section to ensure TOC is isolated.
|
||||
|
||||
## Images & Media
|
||||
```javascript
|
||||
// Basic image with sizing & positioning
|
||||
// CRITICAL: Always specify 'type' parameter - it's REQUIRED for ImageRun
|
||||
new Paragraph({
|
||||
alignment: AlignmentType.CENTER,
|
||||
children: [new ImageRun({
|
||||
type: "png", // NEW REQUIREMENT: Must specify image type (png, jpg, jpeg, gif, bmp, svg)
|
||||
data: fs.readFileSync("image.png"),
|
||||
transformation: { width: 200, height: 150, rotation: 0 }, // rotation in degrees
|
||||
altText: { title: "Logo", description: "Company logo", name: "Name" } // IMPORTANT: All three fields are required
|
||||
})]
|
||||
})
|
||||
```
|
||||
|
||||
## Page Breaks
|
||||
```javascript
|
||||
// Manual page break
|
||||
new Paragraph({ children: [new PageBreak()] }),
|
||||
|
||||
// Page break before paragraph
|
||||
new Paragraph({
|
||||
pageBreakBefore: true,
|
||||
children: [new TextRun("This starts on a new page")]
|
||||
})
|
||||
|
||||
// ⚠️ CRITICAL: NEVER use PageBreak standalone - it will create invalid XML that Word cannot open
|
||||
// ❌ WRONG: new PageBreak()
|
||||
// ✅ CORRECT: new Paragraph({ children: [new PageBreak()] })
|
||||
```
|
||||
|
||||
## Cover Page
|
||||
**If the document has a cover page, the cover content should be centered both horizontally and vertically.**
|
||||
|
||||
**Important notes for cover pages:**
|
||||
- **Horizontal centering**: Use `alignment: AlignmentType.CENTER` on all cover page paragraphs
|
||||
- **Vertical centering**: Use `spacing: { before: XXXX }` on elements to visually center content (adjust based on page height)
|
||||
- **Separate section**: Create a dedicated section for the cover page to separate it from main content
|
||||
- **Page break**: Use `new Paragraph({ children: [new PageBreak()] })` at the start of the next section to ensure cover is isolated
|
||||
|
||||
## Headers/Footers & Page Setup
|
||||
```javascript
|
||||
const doc = new Document({
|
||||
sections: [{
|
||||
properties: {
|
||||
page: {
|
||||
margin: { top: 1440, right: 1440, bottom: 1440, left: 1440 }, // 1440 = 1 inch
|
||||
size: { orientation: PageOrientation.LANDSCAPE },
|
||||
pageNumbers: { start: 1, formatType: "decimal" } // "upperRoman", "lowerRoman", "upperLetter", "lowerLetter"
|
||||
}
|
||||
},
|
||||
headers: {
|
||||
default: new Header({ children: [new Paragraph({
|
||||
alignment: AlignmentType.RIGHT,
|
||||
children: [new TextRun("Header Text")]
|
||||
})] })
|
||||
},
|
||||
footers: {
|
||||
default: new Footer({ children: [new Paragraph({
|
||||
alignment: AlignmentType.CENTER,
|
||||
children: [new TextRun("Page "), new TextRun({ children: [PageNumber.CURRENT] }), new TextRun(" of "), new TextRun({ children: [PageNumber.TOTAL_PAGES] })]
|
||||
})] })
|
||||
},
|
||||
children: [/* content */]
|
||||
}]
|
||||
});
|
||||
```
|
||||
|
||||
## Tabs
|
||||
```javascript
|
||||
new Paragraph({
|
||||
tabStops: [
|
||||
{ type: TabStopType.LEFT, position: TabStopPosition.MAX / 4 },
|
||||
{ type: TabStopType.CENTER, position: TabStopPosition.MAX / 2 },
|
||||
{ type: TabStopType.RIGHT, position: TabStopPosition.MAX * 3 / 4 }
|
||||
],
|
||||
children: [new TextRun("Left\tCenter\tRight")]
|
||||
})
|
||||
```
|
||||
|
||||
## Constants & Quick Reference
|
||||
- **Underlines:** `SINGLE`, `DOUBLE`, `WAVY`, `DASH`
|
||||
- **Borders:** `SINGLE`, `DOUBLE`, `DASHED`, `DOTTED`
|
||||
- **Numbering:** `DECIMAL` (1,2,3), `UPPER_ROMAN` (I,II,III), `LOWER_LETTER` (a,b,c)
|
||||
- **Tabs:** `LEFT`, `CENTER`, `RIGHT`, `DECIMAL`
|
||||
- **Symbols:** `"2022"` (•), `"00A9"` (©), `"00AE"` (®), `"2122"` (™), `"00B0"` (°), `"F070"` (✓), `"F0FC"` (✗)
|
||||
|
||||
## Critical Issues & Common Mistakes
|
||||
- **CRITICAL for cover pages**: If the document has a cover page, the cover content should be centered both horizontally (AlignmentType.CENTER) and vertically (use spacing.before to adjust)
|
||||
- **CRITICAL: PageBreak must ALWAYS be inside a Paragraph** - standalone PageBreak creates invalid XML that Word cannot open
|
||||
- **ALWAYS use ShadingType.CLEAR for table cell shading** - Never use ShadingType.SOLID (causes black background).
|
||||
- Measurements in DXA (1440 = 1 inch) | Each table cell needs ≥1 Paragraph | If TOC is added, it requires HeadingLevel styles only
|
||||
- **CRITICAL: ALWAYS use system-installed fonts** - Times New Roman for English, SimSun for Chinese - NEVER download custom fonts unless absolutely necessary
|
||||
- **ALWAYS use custom styles** with appropriate system fonts for professional appearance and proper visual hierarchy
|
||||
- **ALWAYS set a default font** using `styles.default.document.run.font` - **Times New Roman** for English, **SimSun** for Chinese
|
||||
- **CRITICAL for Chinese documents**: Use SimSun for body text, SimHei ONLY for headings - NEVER use SimHei for entire document
|
||||
- **CRITICAL for Chinese body text**: Add first-line indent with `indent: { firstLine: 480 }` (approximately 2 characters for 12pt font)
|
||||
- **ALWAYS use columnWidths array for tables** + individual cell widths for compatibility
|
||||
- **NEVER use unicode symbols for bullets** - always use proper numbering configuration with `LevelFormat.BULLET` constant (NOT the string "bullet")
|
||||
- **NEVER use \n for line breaks anywhere** - always use separate Paragraph elements for each line
|
||||
- **ALWAYS use TextRun objects within Paragraph children** - never use text property directly on Paragraph
|
||||
- **CRITICAL for images**: ImageRun REQUIRES `type` parameter - always specify "png", "jpg", "jpeg", "gif", "bmp", or "svg"
|
||||
- **CRITICAL for bullets**: Must use `LevelFormat.BULLET` constant, not string "bullet", and include `text: "•"` for the bullet character
|
||||
- **CRITICAL for numbering**: Each numbering reference creates an INDEPENDENT list. Same reference = continues numbering (1,2,3 then 4,5,6). Different reference = restarts at 1 (1,2,3 then 1,2,3). Use unique reference names for each separate numbered section!
|
||||
- **CRITICAL for TOC**: When using TableOfContents, headings must use HeadingLevel ONLY - do NOT add custom styles to heading paragraphs or TOC will break.
|
||||
- **CRITICAL for Tables**: Set `columnWidths` array + individual cell widths, apply borders to cells not table
|
||||
- **MANDATORY for Tables**: ALWAYS set `margins` at Table level - this prevents text from touching borders and is required for professional quality. NEVER omit this property.
|
||||
- **Set table margins at TABLE level** for consistent cell padding (avoids repetition per cell)
|
||||
Executable
+615
@@ -0,0 +1,615 @@
|
||||
# Office Open XML Technical Reference
|
||||
|
||||
**Important: Read this entire document before starting.** This document covers:
|
||||
- [Technical Guidelines](#technical-guidelines) - Schema compliance rules and validation requirements
|
||||
- [Document Content Patterns](#document-content-patterns) - XML patterns for headings, lists, tables, formatting, etc.
|
||||
- [Document Library (Python)](#document-library-python) - Recommended approach for OOXML manipulation with automatic infrastructure setup
|
||||
- [Tracked Changes (Redlining)](#tracked-changes-redlining) - XML patterns for implementing tracked changes
|
||||
|
||||
## Technical Guidelines
|
||||
|
||||
### Schema Compliance
|
||||
- **Element ordering in `<w:pPr>`**: `<w:pStyle>`, `<w:numPr>`, `<w:spacing>`, `<w:ind>`, `<w:jc>`
|
||||
- **Whitespace**: Add `xml:space='preserve'` to `<w:t>` elements with leading/trailing spaces
|
||||
- **Unicode**: Escape characters in ASCII content: `"` becomes `“`
|
||||
- **Character encoding reference**: Curly quotes `""` become `“”`, apostrophe `'` becomes `’`, em-dash `—` becomes `—`
|
||||
- **Tracked changes**: Use `<w:del>` and `<w:ins>` tags with `w:author="GLM"` outside `<w:r>` elements
|
||||
- **Critical**: `<w:ins>` closes with `</w:ins>`, `<w:del>` closes with `</w:del>` - never mix
|
||||
- **RSIDs must be 8-digit hex**: Use values like `00AB1234` (only 0-9, A-F characters)
|
||||
- **trackRevisions placement**: Add `<w:trackRevisions/>` after `<w:proofState>` in settings.xml
|
||||
- **Images**: Add to `word/media/`, reference in `document.xml`, set dimensions to prevent overflow
|
||||
|
||||
## Document Content Patterns
|
||||
|
||||
### Basic Structure
|
||||
```xml
|
||||
<w:p>
|
||||
<w:r><w:t>Text content</w:t></w:r>
|
||||
</w:p>
|
||||
```
|
||||
|
||||
### Headings and Styles
|
||||
```xml
|
||||
<w:p>
|
||||
<w:pPr>
|
||||
<w:pStyle w:val="Title"/>
|
||||
<w:jc w:val="center"/>
|
||||
</w:pPr>
|
||||
<w:r><w:t>Document Title</w:t></w:r>
|
||||
</w:p>
|
||||
|
||||
<w:p>
|
||||
<w:pPr><w:pStyle w:val="Heading2"/></w:pPr>
|
||||
<w:r><w:t>Section Heading</w:t></w:r>
|
||||
</w:p>
|
||||
```
|
||||
|
||||
### Text Formatting
|
||||
```xml
|
||||
<!-- Bold -->
|
||||
<w:r><w:rPr><w:b/><w:bCs/></w:rPr><w:t>Bold</w:t></w:r>
|
||||
<!-- Italic -->
|
||||
<w:r><w:rPr><w:i/><w:iCs/></w:rPr><w:t>Italic</w:t></w:r>
|
||||
<!-- Underline -->
|
||||
<w:r><w:rPr><w:u w:val="single"/></w:rPr><w:t>Underlined</w:t></w:r>
|
||||
<!-- Highlight -->
|
||||
<w:r><w:rPr><w:highlight w:val="yellow"/></w:rPr><w:t>Highlighted</w:t></w:r>
|
||||
```
|
||||
|
||||
### Lists
|
||||
```xml
|
||||
<!-- Numbered list -->
|
||||
<w:p>
|
||||
<w:pPr>
|
||||
<w:pStyle w:val="ListParagraph"/>
|
||||
<w:numPr><w:ilvl w:val="0"/><w:numId w:val="1"/></w:numPr>
|
||||
<w:spacing w:before="240"/>
|
||||
</w:pPr>
|
||||
<w:r><w:t>First item</w:t></w:r>
|
||||
</w:p>
|
||||
|
||||
<!-- Restart numbered list at 1 - use different numId -->
|
||||
<w:p>
|
||||
<w:pPr>
|
||||
<w:pStyle w:val="ListParagraph"/>
|
||||
<w:numPr><w:ilvl w:val="0"/><w:numId w:val="2"/></w:numPr>
|
||||
<w:spacing w:before="240"/>
|
||||
</w:pPr>
|
||||
<w:r><w:t>New list item 1</w:t></w:r>
|
||||
</w:p>
|
||||
|
||||
<!-- Bullet list (level 2) -->
|
||||
<w:p>
|
||||
<w:pPr>
|
||||
<w:pStyle w:val="ListParagraph"/>
|
||||
<w:numPr><w:ilvl w:val="1"/><w:numId w:val="1"/></w:numPr>
|
||||
<w:spacing w:before="240"/>
|
||||
<w:ind w:left="900"/>
|
||||
</w:pPr>
|
||||
<w:r><w:t>Bullet item</w:t></w:r>
|
||||
</w:p>
|
||||
```
|
||||
|
||||
### Tables
|
||||
```xml
|
||||
<w:tbl>
|
||||
<w:tblPr>
|
||||
<w:tblStyle w:val="TableGrid"/>
|
||||
<w:tblW w:w="0" w:type="auto"/>
|
||||
</w:tblPr>
|
||||
<w:tblGrid>
|
||||
<w:gridCol w:w="4675"/><w:gridCol w:w="4675"/>
|
||||
</w:tblGrid>
|
||||
<w:tr>
|
||||
<w:tc>
|
||||
<w:tcPr><w:tcW w:w="4675" w:type="dxa"/></w:tcPr>
|
||||
<w:p><w:r><w:t>Cell 1</w:t></w:r></w:p>
|
||||
</w:tc>
|
||||
<w:tc>
|
||||
<w:tcPr><w:tcW w:w="4675" w:type="dxa"/></w:tcPr>
|
||||
<w:p><w:r><w:t>Cell 2</w:t></w:r></w:p>
|
||||
</w:tc>
|
||||
</w:tr>
|
||||
</w:tbl>
|
||||
```
|
||||
|
||||
### Layout
|
||||
```xml
|
||||
<!-- Page break before new section (common pattern) -->
|
||||
<w:p>
|
||||
<w:r>
|
||||
<w:br w:type="page"/>
|
||||
</w:r>
|
||||
</w:p>
|
||||
<w:p>
|
||||
<w:pPr>
|
||||
<w:pStyle w:val="Heading1"/>
|
||||
</w:pPr>
|
||||
<w:r>
|
||||
<w:t>New Section Title</w:t>
|
||||
</w:r>
|
||||
</w:p>
|
||||
|
||||
<!-- Centered paragraph -->
|
||||
<w:p>
|
||||
<w:pPr>
|
||||
<w:spacing w:before="240" w:after="0"/>
|
||||
<w:jc w:val="center"/>
|
||||
</w:pPr>
|
||||
<w:r><w:t>Centered text</w:t></w:r>
|
||||
</w:p>
|
||||
|
||||
<!-- Font change - paragraph level (applies to all runs) -->
|
||||
<w:p>
|
||||
<w:pPr>
|
||||
<w:rPr><w:rFonts w:ascii="Courier New" w:hAnsi="Courier New"/></w:rPr>
|
||||
</w:pPr>
|
||||
<w:r><w:t>Monospace text</w:t></w:r>
|
||||
</w:p>
|
||||
|
||||
<!-- Font change - run level (specific to this text) -->
|
||||
<w:p>
|
||||
<w:r>
|
||||
<w:rPr><w:rFonts w:ascii="Courier New" w:hAnsi="Courier New"/></w:rPr>
|
||||
<w:t>This text is Courier New</w:t>
|
||||
</w:r>
|
||||
<w:r><w:t> and this text uses default font</w:t></w:r>
|
||||
</w:p>
|
||||
```
|
||||
|
||||
## File Updates
|
||||
|
||||
When adding content, update these files:
|
||||
|
||||
**`word/_rels/document.xml.rels`:**
|
||||
```xml
|
||||
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering" Target="numbering.xml"/>
|
||||
<Relationship Id="rId5" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="media/image1.png"/>
|
||||
```
|
||||
|
||||
**`[Content_Types].xml`:**
|
||||
```xml
|
||||
<Default Extension="png" ContentType="image/png"/>
|
||||
<Override PartName="/word/numbering.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml"/>
|
||||
```
|
||||
|
||||
### Images
|
||||
**CRITICAL**: Calculate dimensions to prevent page overflow and maintain aspect ratio.
|
||||
|
||||
```xml
|
||||
<!-- Minimal required structure -->
|
||||
<w:p>
|
||||
<w:r>
|
||||
<w:drawing>
|
||||
<wp:inline>
|
||||
<wp:extent cx="2743200" cy="1828800"/>
|
||||
<wp:docPr id="1" name="Picture 1"/>
|
||||
<a:graphic xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
|
||||
<a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/picture">
|
||||
<pic:pic xmlns:pic="http://schemas.openxmlformats.org/drawingml/2006/picture">
|
||||
<pic:nvPicPr>
|
||||
<pic:cNvPr id="0" name="image1.png"/>
|
||||
<pic:cNvPicPr/>
|
||||
</pic:nvPicPr>
|
||||
<pic:blipFill>
|
||||
<a:blip r:embed="rId5"/>
|
||||
<!-- Add for stretch fill with aspect ratio preservation -->
|
||||
<a:stretch>
|
||||
<a:fillRect/>
|
||||
</a:stretch>
|
||||
</pic:blipFill>
|
||||
<pic:spPr>
|
||||
<a:xfrm>
|
||||
<a:ext cx="2743200" cy="1828800"/>
|
||||
</a:xfrm>
|
||||
<a:prstGeom prst="rect"/>
|
||||
</pic:spPr>
|
||||
</pic:pic>
|
||||
</a:graphicData>
|
||||
</a:graphic>
|
||||
</wp:inline>
|
||||
</w:drawing>
|
||||
</w:r>
|
||||
</w:p>
|
||||
```
|
||||
|
||||
### Links (Hyperlinks)
|
||||
|
||||
**IMPORTANT**: All hyperlinks (both internal and external) require the Hyperlink style to be defined in styles.xml. Without this style, links will look like regular text instead of blue underlined clickable links.
|
||||
|
||||
**External Links:**
|
||||
```xml
|
||||
<!-- In document.xml -->
|
||||
<w:hyperlink r:id="rId5">
|
||||
<w:r>
|
||||
<w:rPr><w:rStyle w:val="Hyperlink"/></w:rPr>
|
||||
<w:t>Link Text</w:t>
|
||||
</w:r>
|
||||
</w:hyperlink>
|
||||
|
||||
<!-- In word/_rels/document.xml.rels -->
|
||||
<Relationship Id="rId5" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink"
|
||||
Target="https://www.example.com/" TargetMode="External"/>
|
||||
```
|
||||
|
||||
**Internal Links:**
|
||||
|
||||
```xml
|
||||
<!-- Link to bookmark -->
|
||||
<w:hyperlink w:anchor="myBookmark">
|
||||
<w:r>
|
||||
<w:rPr><w:rStyle w:val="Hyperlink"/></w:rPr>
|
||||
<w:t>Link Text</w:t>
|
||||
</w:r>
|
||||
</w:hyperlink>
|
||||
|
||||
<!-- Bookmark target -->
|
||||
<w:bookmarkStart w:id="0" w:name="myBookmark"/>
|
||||
<w:r><w:t>Target content</w:t></w:r>
|
||||
<w:bookmarkEnd w:id="0"/>
|
||||
```
|
||||
|
||||
**Hyperlink Style (required in styles.xml):**
|
||||
```xml
|
||||
<w:style w:type="character" w:styleId="Hyperlink">
|
||||
<w:name w:val="Hyperlink"/>
|
||||
<w:basedOn w:val="DefaultParagraphFont"/>
|
||||
<w:uiPriority w:val="99"/>
|
||||
<w:unhideWhenUsed/>
|
||||
<w:rPr>
|
||||
<w:color w:val="467886" w:themeColor="hyperlink"/>
|
||||
<w:u w:val="single"/>
|
||||
</w:rPr>
|
||||
</w:style>
|
||||
```
|
||||
|
||||
## Document Library (Python)
|
||||
|
||||
Use the Document class from `scripts/document.py` for all tracked changes and comments. It automatically handles infrastructure setup (people.xml, RSIDs, settings.xml, comment files, relationships, content types). Only use direct XML manipulation for complex scenarios not supported by the library.
|
||||
|
||||
**Working with Unicode and Entities:**
|
||||
- **Searching**: Both entity notation and Unicode characters work - `contains="“Company"` and `contains="\u201cCompany"` find the same text
|
||||
- **Replacing**: Use either entities (`“`) or Unicode (`\u201c`) - both work and will be converted appropriately based on the file's encoding (ascii → entities, utf-8 → Unicode)
|
||||
|
||||
### Initialization
|
||||
|
||||
**Find the docx skill root** (directory containing `scripts/` and `ooxml/`):
|
||||
```bash
|
||||
# Search for document.py to locate the skill root
|
||||
# Note: /mnt/skills is used here as an example; check your context for the actual location
|
||||
find /mnt/skills -name "document.py" -path "*/docx/scripts/*" 2>/dev/null | head -1
|
||||
# Example output: /mnt/skills/docx/scripts/document.py
|
||||
# Skill root is: /mnt/skills/docx
|
||||
```
|
||||
|
||||
**Run your script with PYTHONPATH** set to the docx skill root:
|
||||
```bash
|
||||
PYTHONPATH=/mnt/skills/docx python your_script.py
|
||||
```
|
||||
|
||||
**In your script**, import from the skill root:
|
||||
```python
|
||||
from scripts.document import Document, DocxXMLEditor
|
||||
|
||||
# Basic initialization (automatically creates temp copy and sets up infrastructure)
|
||||
doc = Document('unpacked')
|
||||
|
||||
# Customize author and initials
|
||||
doc = Document('unpacked', author="John Doe", initials="JD")
|
||||
|
||||
# Enable track revisions mode
|
||||
doc = Document('unpacked', track_revisions=True)
|
||||
|
||||
# Specify custom RSID (auto-generated if not provided)
|
||||
doc = Document('unpacked', rsid="07DC5ECB")
|
||||
```
|
||||
|
||||
### Creating Tracked Changes
|
||||
|
||||
**CRITICAL**: Only mark text that actually changes. Keep ALL unchanged text outside `<w:del>`/`<w:ins>` tags. Marking unchanged text makes edits unprofessional and harder to review.
|
||||
|
||||
**Attribute Handling**: The Document class auto-injects attributes (w:id, w:date, w:rsidR, w:rsidDel, w16du:dateUtc, xml:space) into new elements. When preserving unchanged text from the original document, copy the original `<w:r>` element with its existing attributes to maintain document integrity.
|
||||
|
||||
**Method Selection Guide**:
|
||||
- **Adding your own changes to regular text**: Use `replace_node()` with `<w:del>`/`<w:ins>` tags, or `suggest_deletion()` for removing entire `<w:r>` or `<w:p>` elements
|
||||
- **Partially modifying another author's tracked change**: Use `replace_node()` to nest your changes inside their `<w:ins>`/`<w:del>`
|
||||
- **Completely rejecting another author's insertion**: Use `revert_insertion()` on the `<w:ins>` element (NOT `suggest_deletion()`)
|
||||
- **Completely rejecting another author's deletion**: Use `revert_deletion()` on the `<w:del>` element to restore deleted content using tracked changes
|
||||
|
||||
```python
|
||||
# Minimal edit - change one word: "The report is monthly" → "The report is quarterly"
|
||||
# Original: <w:r w:rsidR="00AB12CD"><w:rPr><w:rFonts w:ascii="Calibri"/></w:rPr><w:t>The report is monthly</w:t></w:r>
|
||||
node = doc["word/document.xml"].get_node(tag="w:r", contains="The report is monthly")
|
||||
rpr = tags[0].toxml() if (tags := node.getElementsByTagName("w:rPr")) else ""
|
||||
replacement = f'<w:r w:rsidR="00AB12CD">{rpr}<w:t>The report is </w:t></w:r><w:del><w:r>{rpr}<w:delText>monthly</w:delText></w:r></w:del><w:ins><w:r>{rpr}<w:t>quarterly</w:t></w:r></w:ins>'
|
||||
doc["word/document.xml"].replace_node(node, replacement)
|
||||
|
||||
# Minimal edit - change number: "within 30 days" → "within 45 days"
|
||||
# Original: <w:r w:rsidR="00XYZ789"><w:rPr><w:rFonts w:ascii="Calibri"/></w:rPr><w:t>within 30 days</w:t></w:r>
|
||||
node = doc["word/document.xml"].get_node(tag="w:r", contains="within 30 days")
|
||||
rpr = tags[0].toxml() if (tags := node.getElementsByTagName("w:rPr")) else ""
|
||||
replacement = f'<w:r w:rsidR="00XYZ789">{rpr}<w:t>within </w:t></w:r><w:del><w:r>{rpr}<w:delText>30</w:delText></w:r></w:del><w:ins><w:r>{rpr}<w:t>45</w:t></w:r></w:ins><w:r w:rsidR="00XYZ789">{rpr}<w:t> days</w:t></w:r>'
|
||||
doc["word/document.xml"].replace_node(node, replacement)
|
||||
|
||||
# Complete replacement - preserve formatting even when replacing all text
|
||||
node = doc["word/document.xml"].get_node(tag="w:r", contains="apple")
|
||||
rpr = tags[0].toxml() if (tags := node.getElementsByTagName("w:rPr")) else ""
|
||||
replacement = f'<w:del><w:r>{rpr}<w:delText>apple</w:delText></w:r></w:del><w:ins><w:r>{rpr}<w:t>banana orange</w:t></w:r></w:ins>'
|
||||
doc["word/document.xml"].replace_node(node, replacement)
|
||||
|
||||
# Insert new content (no attributes needed - auto-injected)
|
||||
node = doc["word/document.xml"].get_node(tag="w:r", contains="existing text")
|
||||
doc["word/document.xml"].insert_after(node, '<w:ins><w:r><w:t>new text</w:t></w:r></w:ins>')
|
||||
|
||||
# Partially delete another author's insertion
|
||||
# Original: <w:ins w:author="Jane Smith" w:date="..."><w:r><w:t>quarterly financial report</w:t></w:r></w:ins>
|
||||
# Goal: Delete only "financial" to make it "quarterly report"
|
||||
node = doc["word/document.xml"].get_node(tag="w:ins", attrs={"w:id": "5"})
|
||||
# IMPORTANT: Preserve w:author="Jane Smith" on the outer <w:ins> to maintain authorship
|
||||
replacement = '''<w:ins w:author="Jane Smith" w:date="2025-01-15T10:00:00Z">
|
||||
<w:r><w:t>quarterly </w:t></w:r>
|
||||
<w:del><w:r><w:delText>financial </w:delText></w:r></w:del>
|
||||
<w:r><w:t>report</w:t></w:r>
|
||||
</w:ins>'''
|
||||
doc["word/document.xml"].replace_node(node, replacement)
|
||||
|
||||
# Change part of another author's insertion
|
||||
# Original: <w:ins w:author="Jane Smith"><w:r><w:t>in silence, safe and sound</w:t></w:r></w:ins>
|
||||
# Goal: Change "safe and sound" to "soft and unbound"
|
||||
node = doc["word/document.xml"].get_node(tag="w:ins", attrs={"w:id": "8"})
|
||||
replacement = f'''<w:ins w:author="Jane Smith" w:date="2025-01-15T10:00:00Z">
|
||||
<w:r><w:t>in silence, </w:t></w:r>
|
||||
</w:ins>
|
||||
<w:ins>
|
||||
<w:r><w:t>soft and unbound</w:t></w:r>
|
||||
</w:ins>
|
||||
<w:ins w:author="Jane Smith" w:date="2025-01-15T10:00:00Z">
|
||||
<w:del><w:r><w:delText>safe and sound</w:delText></w:r></w:del>
|
||||
</w:ins>'''
|
||||
doc["word/document.xml"].replace_node(node, replacement)
|
||||
|
||||
# Delete entire run (use only when deleting all content; use replace_node for partial deletions)
|
||||
node = doc["word/document.xml"].get_node(tag="w:r", contains="text to delete")
|
||||
doc["word/document.xml"].suggest_deletion(node)
|
||||
|
||||
# Delete entire paragraph (in-place, handles both regular and numbered list paragraphs)
|
||||
para = doc["word/document.xml"].get_node(tag="w:p", contains="paragraph to delete")
|
||||
doc["word/document.xml"].suggest_deletion(para)
|
||||
|
||||
# Add new numbered list item
|
||||
target_para = doc["word/document.xml"].get_node(tag="w:p", contains="existing list item")
|
||||
pPr = tags[0].toxml() if (tags := target_para.getElementsByTagName("w:pPr")) else ""
|
||||
new_item = f'<w:p>{pPr}<w:r><w:t>New item</w:t></w:r></w:p>'
|
||||
tracked_para = DocxXMLEditor.suggest_paragraph(new_item)
|
||||
doc["word/document.xml"].insert_after(target_para, tracked_para)
|
||||
# Optional: add spacing paragraph before content for better visual separation
|
||||
# spacing = DocxXMLEditor.suggest_paragraph('<w:p><w:pPr><w:pStyle w:val="ListParagraph"/></w:pPr></w:p>')
|
||||
# doc["word/document.xml"].insert_after(target_para, spacing + tracked_para)
|
||||
```
|
||||
|
||||
### Adding Comments
|
||||
|
||||
Comments are added with the author name "Z.ai" by default. Initialize the Document with custom author if needed:
|
||||
|
||||
```python
|
||||
# Initialize with Z.ai as author (recommended)
|
||||
doc = Document('unpacked', author="Z.ai", initials="Z")
|
||||
|
||||
# Add comment spanning two existing tracked changes
|
||||
# Note: w:id is auto-generated. Only search by w:id if you know it from XML inspection
|
||||
start_node = doc["word/document.xml"].get_node(tag="w:del", attrs={"w:id": "1"})
|
||||
end_node = doc["word/document.xml"].get_node(tag="w:ins", attrs={"w:id": "2"})
|
||||
doc.add_comment(start=start_node, end=end_node, text="Explanation of this change")
|
||||
|
||||
# Add comment on a paragraph
|
||||
para = doc["word/document.xml"].get_node(tag="w:p", contains="paragraph text")
|
||||
doc.add_comment(start=para, end=para, text="Comment on this paragraph")
|
||||
|
||||
# Add comment on newly created tracked change
|
||||
# First create the tracked change
|
||||
node = doc["word/document.xml"].get_node(tag="w:r", contains="old")
|
||||
new_nodes = doc["word/document.xml"].replace_node(
|
||||
node,
|
||||
'<w:del><w:r><w:delText>old</w:delText></w:r></w:del><w:ins><w:r><w:t>new</w:t></w:r></w:ins>'
|
||||
)
|
||||
# Then add comment on the newly created elements
|
||||
# new_nodes[0] is the <w:del>, new_nodes[1] is the <w:ins>
|
||||
doc.add_comment(start=new_nodes[0], end=new_nodes[1], text="Changed old to new per requirements")
|
||||
|
||||
# Reply to existing comment
|
||||
doc.reply_to_comment(parent_comment_id=0, text="I agree with this change")
|
||||
```
|
||||
|
||||
### Rejecting Tracked Changes
|
||||
|
||||
**IMPORTANT**: Use `revert_insertion()` to reject insertions and `revert_deletion()` to restore deletions using tracked changes. Use `suggest_deletion()` only for regular unmarked content.
|
||||
|
||||
```python
|
||||
# Reject insertion (wraps it in deletion)
|
||||
# Use this when another author inserted text that you want to delete
|
||||
ins = doc["word/document.xml"].get_node(tag="w:ins", attrs={"w:id": "5"})
|
||||
nodes = doc["word/document.xml"].revert_insertion(ins) # Returns [ins]
|
||||
|
||||
# Reject deletion (creates insertion to restore deleted content)
|
||||
# Use this when another author deleted text that you want to restore
|
||||
del_elem = doc["word/document.xml"].get_node(tag="w:del", attrs={"w:id": "3"})
|
||||
nodes = doc["word/document.xml"].revert_deletion(del_elem) # Returns [del_elem, new_ins]
|
||||
|
||||
# Reject all insertions in a paragraph
|
||||
para = doc["word/document.xml"].get_node(tag="w:p", contains="paragraph text")
|
||||
nodes = doc["word/document.xml"].revert_insertion(para) # Returns [para]
|
||||
|
||||
# Reject all deletions in a paragraph
|
||||
para = doc["word/document.xml"].get_node(tag="w:p", contains="paragraph text")
|
||||
nodes = doc["word/document.xml"].revert_deletion(para) # Returns [para]
|
||||
```
|
||||
|
||||
### Inserting Images
|
||||
|
||||
**CRITICAL**: The Document class works with a temporary copy at `doc.unpacked_path`. Always copy images to this temp directory, not the original unpacked folder.
|
||||
|
||||
```python
|
||||
from PIL import Image
|
||||
import shutil, os
|
||||
|
||||
# Initialize document first
|
||||
doc = Document('unpacked')
|
||||
|
||||
# Copy image and calculate full-width dimensions with aspect ratio
|
||||
media_dir = os.path.join(doc.unpacked_path, 'word/media')
|
||||
os.makedirs(media_dir, exist_ok=True)
|
||||
shutil.copy('image.png', os.path.join(media_dir, 'image1.png'))
|
||||
img = Image.open(os.path.join(media_dir, 'image1.png'))
|
||||
width_emus = int(6.5 * 914400) # 6.5" usable width, 914400 EMUs/inch
|
||||
height_emus = int(width_emus * img.size[1] / img.size[0])
|
||||
|
||||
# Add relationship and content type
|
||||
rels_editor = doc['word/_rels/document.xml.rels']
|
||||
next_rid = rels_editor.get_next_rid()
|
||||
rels_editor.append_to(rels_editor.dom.documentElement,
|
||||
f'<Relationship Id="{next_rid}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="media/image1.png"/>')
|
||||
doc['[Content_Types].xml'].append_to(doc['[Content_Types].xml'].dom.documentElement,
|
||||
'<Default Extension="png" ContentType="image/png"/>')
|
||||
|
||||
# Insert image
|
||||
node = doc["word/document.xml"].get_node(tag="w:p", line_number=100)
|
||||
doc["word/document.xml"].insert_after(node, f'''<w:p>
|
||||
<w:r>
|
||||
<w:drawing>
|
||||
<wp:inline distT="0" distB="0" distL="0" distR="0">
|
||||
<wp:extent cx="{width_emus}" cy="{height_emus}"/>
|
||||
<wp:docPr id="1" name="Picture 1"/>
|
||||
<a:graphic xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
|
||||
<a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/picture">
|
||||
<pic:pic xmlns:pic="http://schemas.openxmlformats.org/drawingml/2006/picture">
|
||||
<pic:nvPicPr><pic:cNvPr id="1" name="image1.png"/><pic:cNvPicPr/></pic:nvPicPr>
|
||||
<pic:blipFill><a:blip r:embed="{next_rid}"/><a:stretch><a:fillRect/></a:stretch></pic:blipFill>
|
||||
<pic:spPr><a:xfrm><a:ext cx="{width_emus}" cy="{height_emus}"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></pic:spPr>
|
||||
</pic:pic>
|
||||
</a:graphicData>
|
||||
</a:graphic>
|
||||
</wp:inline>
|
||||
</w:drawing>
|
||||
</w:r>
|
||||
</w:p>''')
|
||||
```
|
||||
|
||||
### Getting Nodes
|
||||
|
||||
```python
|
||||
# By text content
|
||||
node = doc["word/document.xml"].get_node(tag="w:p", contains="specific text")
|
||||
|
||||
# By line range
|
||||
para = doc["word/document.xml"].get_node(tag="w:p", line_number=range(100, 150))
|
||||
|
||||
# By attributes
|
||||
node = doc["word/document.xml"].get_node(tag="w:del", attrs={"w:id": "1"})
|
||||
|
||||
# By exact line number (must be line number where tag opens)
|
||||
para = doc["word/document.xml"].get_node(tag="w:p", line_number=42)
|
||||
|
||||
# Combine filters
|
||||
node = doc["word/document.xml"].get_node(tag="w:r", line_number=range(40, 60), contains="text")
|
||||
|
||||
# Disambiguate when text appears multiple times - add line_number range
|
||||
node = doc["word/document.xml"].get_node(tag="w:r", contains="Section", line_number=range(2400, 2500))
|
||||
```
|
||||
|
||||
### Saving
|
||||
|
||||
```python
|
||||
# Save with automatic validation (copies back to original directory)
|
||||
doc.save() # Validates by default, raises error if validation fails
|
||||
|
||||
# Save to different location
|
||||
doc.save('modified-unpacked')
|
||||
|
||||
# Skip validation (debugging only - needing this in production indicates XML issues)
|
||||
doc.save(validate=False)
|
||||
```
|
||||
|
||||
### Direct DOM Manipulation
|
||||
|
||||
For complex scenarios not covered by the library:
|
||||
|
||||
```python
|
||||
# Access any XML file
|
||||
editor = doc["word/document.xml"]
|
||||
editor = doc["word/comments.xml"]
|
||||
|
||||
# Direct DOM access (defusedxml.minidom.Document)
|
||||
node = doc["word/document.xml"].get_node(tag="w:p", line_number=5)
|
||||
parent = node.parentNode
|
||||
parent.removeChild(node)
|
||||
parent.appendChild(node) # Move to end
|
||||
|
||||
# General document manipulation (without tracked changes)
|
||||
old_node = doc["word/document.xml"].get_node(tag="w:p", contains="original text")
|
||||
doc["word/document.xml"].replace_node(old_node, "<w:p><w:r><w:t>replacement text</w:t></w:r></w:p>")
|
||||
|
||||
# Multiple insertions - use return value to maintain order
|
||||
node = doc["word/document.xml"].get_node(tag="w:r", line_number=100)
|
||||
nodes = doc["word/document.xml"].insert_after(node, "<w:r><w:t>A</w:t></w:r>")
|
||||
nodes = doc["word/document.xml"].insert_after(nodes[-1], "<w:r><w:t>B</w:t></w:r>")
|
||||
nodes = doc["word/document.xml"].insert_after(nodes[-1], "<w:r><w:t>C</w:t></w:r>")
|
||||
# Results in: original_node, A, B, C
|
||||
```
|
||||
|
||||
## Tracked Changes (Redlining)
|
||||
|
||||
**Use the Document class above for all tracked changes.** The patterns below are for reference when constructing replacement XML strings.
|
||||
|
||||
### Validation Rules
|
||||
The validator checks that the document text matches the original after reverting GLM's changes. This means:
|
||||
- **NEVER modify text inside another author's `<w:ins>` or `<w:del>` tags**
|
||||
- **ALWAYS use nested deletions** to remove another author's insertions
|
||||
- **Every edit must be properly tracked** with `<w:ins>` or `<w:del>` tags
|
||||
|
||||
### Tracked Change Patterns
|
||||
|
||||
**CRITICAL RULES**:
|
||||
1. Never modify the content inside another author's tracked changes. Always use nested deletions.
|
||||
2. **XML Structure**: Always place `<w:del>` and `<w:ins>` at paragraph level containing complete `<w:r>` elements. Never nest inside `<w:r>` elements - this creates invalid XML that breaks document processing.
|
||||
|
||||
**Text Insertion:**
|
||||
```xml
|
||||
<w:ins w:id="1" w:author="GLM" w:date="2025-07-30T23:05:00Z" w16du:dateUtc="2025-07-31T06:05:00Z">
|
||||
<w:r w:rsidR="00792858">
|
||||
<w:t>inserted text</w:t>
|
||||
</w:r>
|
||||
</w:ins>
|
||||
```
|
||||
|
||||
**Text Deletion:**
|
||||
```xml
|
||||
<w:del w:id="2" w:author="GLM" w:date="2025-07-30T23:05:00Z" w16du:dateUtc="2025-07-31T06:05:00Z">
|
||||
<w:r w:rsidDel="00792858">
|
||||
<w:delText>deleted text</w:delText>
|
||||
</w:r>
|
||||
</w:del>
|
||||
```
|
||||
|
||||
**Deleting Another Author's Insertion (MUST use nested structure):**
|
||||
```xml
|
||||
<!-- Nest deletion inside the original insertion -->
|
||||
<w:ins w:author="Jane Smith" w:id="16">
|
||||
<w:del w:author="GLM" w:id="40">
|
||||
<w:r><w:delText>monthly</w:delText></w:r>
|
||||
</w:del>
|
||||
</w:ins>
|
||||
<w:ins w:author="GLM" w:id="41">
|
||||
<w:r><w:t>weekly</w:t></w:r>
|
||||
</w:ins>
|
||||
```
|
||||
|
||||
**Restoring Another Author's Deletion:**
|
||||
```xml
|
||||
<!-- Leave their deletion unchanged, add new insertion after it -->
|
||||
<w:del w:author="Jane Smith" w:id="50">
|
||||
<w:r><w:delText>within 30 days</w:delText></w:r>
|
||||
</w:del>
|
||||
<w:ins w:author="GLM" w:id="51">
|
||||
<w:r><w:t>within 30 days</w:t></w:r>
|
||||
</w:ins>
|
||||
```
|
||||
+1499
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,146 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
|
||||
xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
|
||||
xmlns="http://schemas.openxmlformats.org/drawingml/2006/chartDrawing"
|
||||
targetNamespace="http://schemas.openxmlformats.org/drawingml/2006/chartDrawing"
|
||||
elementFormDefault="qualified">
|
||||
<xsd:import namespace="http://schemas.openxmlformats.org/drawingml/2006/main"
|
||||
schemaLocation="dml-main.xsd"/>
|
||||
<xsd:complexType name="CT_ShapeNonVisual">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="cNvPr" type="a:CT_NonVisualDrawingProps" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:element name="cNvSpPr" type="a:CT_NonVisualDrawingShapeProps" minOccurs="1" maxOccurs="1"
|
||||
/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_Shape">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="nvSpPr" type="CT_ShapeNonVisual" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:element name="spPr" type="a:CT_ShapeProperties" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:element name="style" type="a:CT_ShapeStyle" minOccurs="0" maxOccurs="1"/>
|
||||
<xsd:element name="txBody" type="a:CT_TextBody" minOccurs="0" maxOccurs="1"/>
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="macro" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="textlink" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="fLocksText" type="xsd:boolean" use="optional" default="true"/>
|
||||
<xsd:attribute name="fPublished" type="xsd:boolean" use="optional" default="false"/>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_ConnectorNonVisual">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="cNvPr" type="a:CT_NonVisualDrawingProps" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:element name="cNvCxnSpPr" type="a:CT_NonVisualConnectorProperties" minOccurs="1"
|
||||
maxOccurs="1"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_Connector">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="nvCxnSpPr" type="CT_ConnectorNonVisual" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:element name="spPr" type="a:CT_ShapeProperties" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:element name="style" type="a:CT_ShapeStyle" minOccurs="0" maxOccurs="1"/>
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="macro" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="fPublished" type="xsd:boolean" use="optional" default="false"/>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_PictureNonVisual">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="cNvPr" type="a:CT_NonVisualDrawingProps" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:element name="cNvPicPr" type="a:CT_NonVisualPictureProperties" minOccurs="1"
|
||||
maxOccurs="1"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_Picture">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="nvPicPr" type="CT_PictureNonVisual" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:element name="blipFill" type="a:CT_BlipFillProperties" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:element name="spPr" type="a:CT_ShapeProperties" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:element name="style" type="a:CT_ShapeStyle" minOccurs="0" maxOccurs="1"/>
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="macro" type="xsd:string" use="optional" default=""/>
|
||||
<xsd:attribute name="fPublished" type="xsd:boolean" use="optional" default="false"/>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_GraphicFrameNonVisual">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="cNvPr" type="a:CT_NonVisualDrawingProps" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:element name="cNvGraphicFramePr" type="a:CT_NonVisualGraphicFrameProperties"
|
||||
minOccurs="1" maxOccurs="1"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_GraphicFrame">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="nvGraphicFramePr" type="CT_GraphicFrameNonVisual" minOccurs="1"
|
||||
maxOccurs="1"/>
|
||||
<xsd:element name="xfrm" type="a:CT_Transform2D" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:element ref="a:graphic" minOccurs="1" maxOccurs="1"/>
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="macro" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="fPublished" type="xsd:boolean" use="optional" default="false"/>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_GroupShapeNonVisual">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="cNvPr" type="a:CT_NonVisualDrawingProps" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:element name="cNvGrpSpPr" type="a:CT_NonVisualGroupDrawingShapeProps" minOccurs="1"
|
||||
maxOccurs="1"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_GroupShape">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="nvGrpSpPr" type="CT_GroupShapeNonVisual" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:element name="grpSpPr" type="a:CT_GroupShapeProperties" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:choice minOccurs="0" maxOccurs="unbounded">
|
||||
<xsd:element name="sp" type="CT_Shape"/>
|
||||
<xsd:element name="grpSp" type="CT_GroupShape"/>
|
||||
<xsd:element name="graphicFrame" type="CT_GraphicFrame"/>
|
||||
<xsd:element name="cxnSp" type="CT_Connector"/>
|
||||
<xsd:element name="pic" type="CT_Picture"/>
|
||||
</xsd:choice>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:group name="EG_ObjectChoices">
|
||||
<xsd:sequence>
|
||||
<xsd:choice minOccurs="1" maxOccurs="1">
|
||||
<xsd:element name="sp" type="CT_Shape"/>
|
||||
<xsd:element name="grpSp" type="CT_GroupShape"/>
|
||||
<xsd:element name="graphicFrame" type="CT_GraphicFrame"/>
|
||||
<xsd:element name="cxnSp" type="CT_Connector"/>
|
||||
<xsd:element name="pic" type="CT_Picture"/>
|
||||
</xsd:choice>
|
||||
</xsd:sequence>
|
||||
</xsd:group>
|
||||
<xsd:simpleType name="ST_MarkerCoordinate">
|
||||
<xsd:restriction base="xsd:double">
|
||||
<xsd:minInclusive value="0.0"/>
|
||||
<xsd:maxInclusive value="1.0"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
<xsd:complexType name="CT_Marker">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="x" type="ST_MarkerCoordinate" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:element name="y" type="ST_MarkerCoordinate" minOccurs="1" maxOccurs="1"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_RelSizeAnchor">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="from" type="CT_Marker"/>
|
||||
<xsd:element name="to" type="CT_Marker"/>
|
||||
<xsd:group ref="EG_ObjectChoices"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_AbsSizeAnchor">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="from" type="CT_Marker"/>
|
||||
<xsd:element name="ext" type="a:CT_PositiveSize2D"/>
|
||||
<xsd:group ref="EG_ObjectChoices"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:group name="EG_Anchor">
|
||||
<xsd:choice>
|
||||
<xsd:element name="relSizeAnchor" type="CT_RelSizeAnchor"/>
|
||||
<xsd:element name="absSizeAnchor" type="CT_AbsSizeAnchor"/>
|
||||
</xsd:choice>
|
||||
</xsd:group>
|
||||
<xsd:complexType name="CT_Drawing">
|
||||
<xsd:sequence>
|
||||
<xsd:group ref="EG_Anchor" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
</xsd:schema>
|
||||
+1085
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
|
||||
xmlns="http://schemas.openxmlformats.org/drawingml/2006/lockedCanvas"
|
||||
xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
|
||||
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
|
||||
elementFormDefault="qualified"
|
||||
targetNamespace="http://schemas.openxmlformats.org/drawingml/2006/lockedCanvas">
|
||||
<xsd:import namespace="http://schemas.openxmlformats.org/drawingml/2006/main"
|
||||
schemaLocation="dml-main.xsd"/>
|
||||
<xsd:element name="lockedCanvas" type="a:CT_GvmlGroupShape"/>
|
||||
</xsd:schema>
|
||||
+3081
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
|
||||
xmlns="http://schemas.openxmlformats.org/drawingml/2006/picture"
|
||||
xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" elementFormDefault="qualified"
|
||||
targetNamespace="http://schemas.openxmlformats.org/drawingml/2006/picture">
|
||||
<xsd:import namespace="http://schemas.openxmlformats.org/drawingml/2006/main"
|
||||
schemaLocation="dml-main.xsd"/>
|
||||
<xsd:complexType name="CT_PictureNonVisual">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="cNvPr" type="a:CT_NonVisualDrawingProps" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:element name="cNvPicPr" type="a:CT_NonVisualPictureProperties" minOccurs="1"
|
||||
maxOccurs="1"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_Picture">
|
||||
<xsd:sequence minOccurs="1" maxOccurs="1">
|
||||
<xsd:element name="nvPicPr" type="CT_PictureNonVisual" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:element name="blipFill" type="a:CT_BlipFillProperties" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:element name="spPr" type="a:CT_ShapeProperties" minOccurs="1" maxOccurs="1"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:element name="pic" type="CT_Picture"/>
|
||||
</xsd:schema>
|
||||
@@ -0,0 +1,185 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
|
||||
xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
|
||||
xmlns="http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing"
|
||||
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
|
||||
targetNamespace="http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing"
|
||||
elementFormDefault="qualified">
|
||||
<xsd:import namespace="http://schemas.openxmlformats.org/drawingml/2006/main"
|
||||
schemaLocation="dml-main.xsd"/>
|
||||
<xsd:import schemaLocation="shared-relationshipReference.xsd"
|
||||
namespace="http://schemas.openxmlformats.org/officeDocument/2006/relationships"/>
|
||||
<xsd:element name="from" type="CT_Marker"/>
|
||||
<xsd:element name="to" type="CT_Marker"/>
|
||||
<xsd:complexType name="CT_AnchorClientData">
|
||||
<xsd:attribute name="fLocksWithSheet" type="xsd:boolean" use="optional" default="true"/>
|
||||
<xsd:attribute name="fPrintsWithSheet" type="xsd:boolean" use="optional" default="true"/>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_ShapeNonVisual">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="cNvPr" type="a:CT_NonVisualDrawingProps" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:element name="cNvSpPr" type="a:CT_NonVisualDrawingShapeProps" minOccurs="1" maxOccurs="1"
|
||||
/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_Shape">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="nvSpPr" type="CT_ShapeNonVisual" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:element name="spPr" type="a:CT_ShapeProperties" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:element name="style" type="a:CT_ShapeStyle" minOccurs="0" maxOccurs="1"/>
|
||||
<xsd:element name="txBody" type="a:CT_TextBody" minOccurs="0" maxOccurs="1"/>
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="macro" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="textlink" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="fLocksText" type="xsd:boolean" use="optional" default="true"/>
|
||||
<xsd:attribute name="fPublished" type="xsd:boolean" use="optional" default="false"/>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_ConnectorNonVisual">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="cNvPr" type="a:CT_NonVisualDrawingProps" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:element name="cNvCxnSpPr" type="a:CT_NonVisualConnectorProperties" minOccurs="1"
|
||||
maxOccurs="1"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_Connector">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="nvCxnSpPr" type="CT_ConnectorNonVisual" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:element name="spPr" type="a:CT_ShapeProperties" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:element name="style" type="a:CT_ShapeStyle" minOccurs="0" maxOccurs="1"/>
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="macro" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="fPublished" type="xsd:boolean" use="optional" default="false"/>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_PictureNonVisual">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="cNvPr" type="a:CT_NonVisualDrawingProps" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:element name="cNvPicPr" type="a:CT_NonVisualPictureProperties" minOccurs="1"
|
||||
maxOccurs="1"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_Picture">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="nvPicPr" type="CT_PictureNonVisual" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:element name="blipFill" type="a:CT_BlipFillProperties" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:element name="spPr" type="a:CT_ShapeProperties" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:element name="style" type="a:CT_ShapeStyle" minOccurs="0" maxOccurs="1"/>
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="macro" type="xsd:string" use="optional" default=""/>
|
||||
<xsd:attribute name="fPublished" type="xsd:boolean" use="optional" default="false"/>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_GraphicalObjectFrameNonVisual">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="cNvPr" type="a:CT_NonVisualDrawingProps" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:element name="cNvGraphicFramePr" type="a:CT_NonVisualGraphicFrameProperties"
|
||||
minOccurs="1" maxOccurs="1"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_GraphicalObjectFrame">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="nvGraphicFramePr" type="CT_GraphicalObjectFrameNonVisual" minOccurs="1"
|
||||
maxOccurs="1"/>
|
||||
<xsd:element name="xfrm" type="a:CT_Transform2D" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:element ref="a:graphic" minOccurs="1" maxOccurs="1"/>
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="macro" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="fPublished" type="xsd:boolean" use="optional" default="false"/>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_GroupShapeNonVisual">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="cNvPr" type="a:CT_NonVisualDrawingProps" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:element name="cNvGrpSpPr" type="a:CT_NonVisualGroupDrawingShapeProps" minOccurs="1"
|
||||
maxOccurs="1"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_GroupShape">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="nvGrpSpPr" type="CT_GroupShapeNonVisual" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:element name="grpSpPr" type="a:CT_GroupShapeProperties" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:choice minOccurs="0" maxOccurs="unbounded">
|
||||
<xsd:element name="sp" type="CT_Shape"/>
|
||||
<xsd:element name="grpSp" type="CT_GroupShape"/>
|
||||
<xsd:element name="graphicFrame" type="CT_GraphicalObjectFrame"/>
|
||||
<xsd:element name="cxnSp" type="CT_Connector"/>
|
||||
<xsd:element name="pic" type="CT_Picture"/>
|
||||
</xsd:choice>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:group name="EG_ObjectChoices">
|
||||
<xsd:sequence>
|
||||
<xsd:choice minOccurs="1" maxOccurs="1">
|
||||
<xsd:element name="sp" type="CT_Shape"/>
|
||||
<xsd:element name="grpSp" type="CT_GroupShape"/>
|
||||
<xsd:element name="graphicFrame" type="CT_GraphicalObjectFrame"/>
|
||||
<xsd:element name="cxnSp" type="CT_Connector"/>
|
||||
<xsd:element name="pic" type="CT_Picture"/>
|
||||
<xsd:element name="contentPart" type="CT_Rel"/>
|
||||
</xsd:choice>
|
||||
</xsd:sequence>
|
||||
</xsd:group>
|
||||
<xsd:complexType name="CT_Rel">
|
||||
<xsd:attribute ref="r:id" use="required"/>
|
||||
</xsd:complexType>
|
||||
<xsd:simpleType name="ST_ColID">
|
||||
<xsd:restriction base="xsd:int">
|
||||
<xsd:minInclusive value="0"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
<xsd:simpleType name="ST_RowID">
|
||||
<xsd:restriction base="xsd:int">
|
||||
<xsd:minInclusive value="0"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
<xsd:complexType name="CT_Marker">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="col" type="ST_ColID"/>
|
||||
<xsd:element name="colOff" type="a:ST_Coordinate"/>
|
||||
<xsd:element name="row" type="ST_RowID"/>
|
||||
<xsd:element name="rowOff" type="a:ST_Coordinate"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:simpleType name="ST_EditAs">
|
||||
<xsd:restriction base="xsd:token">
|
||||
<xsd:enumeration value="twoCell"/>
|
||||
<xsd:enumeration value="oneCell"/>
|
||||
<xsd:enumeration value="absolute"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
<xsd:complexType name="CT_TwoCellAnchor">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="from" type="CT_Marker"/>
|
||||
<xsd:element name="to" type="CT_Marker"/>
|
||||
<xsd:group ref="EG_ObjectChoices"/>
|
||||
<xsd:element name="clientData" type="CT_AnchorClientData" minOccurs="1" maxOccurs="1"/>
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="editAs" type="ST_EditAs" use="optional" default="twoCell"/>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_OneCellAnchor">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="from" type="CT_Marker"/>
|
||||
<xsd:element name="ext" type="a:CT_PositiveSize2D"/>
|
||||
<xsd:group ref="EG_ObjectChoices"/>
|
||||
<xsd:element name="clientData" type="CT_AnchorClientData" minOccurs="1" maxOccurs="1"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_AbsoluteAnchor">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="pos" type="a:CT_Point2D"/>
|
||||
<xsd:element name="ext" type="a:CT_PositiveSize2D"/>
|
||||
<xsd:group ref="EG_ObjectChoices"/>
|
||||
<xsd:element name="clientData" type="CT_AnchorClientData" minOccurs="1" maxOccurs="1"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:group name="EG_Anchor">
|
||||
<xsd:choice>
|
||||
<xsd:element name="twoCellAnchor" type="CT_TwoCellAnchor"/>
|
||||
<xsd:element name="oneCellAnchor" type="CT_OneCellAnchor"/>
|
||||
<xsd:element name="absoluteAnchor" type="CT_AbsoluteAnchor"/>
|
||||
</xsd:choice>
|
||||
</xsd:group>
|
||||
<xsd:complexType name="CT_Drawing">
|
||||
<xsd:sequence>
|
||||
<xsd:group ref="EG_Anchor" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:element name="wsDr" type="CT_Drawing"/>
|
||||
</xsd:schema>
|
||||
+287
@@ -0,0 +1,287 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
|
||||
xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
|
||||
xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
|
||||
xmlns:dpct="http://schemas.openxmlformats.org/drawingml/2006/picture"
|
||||
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
|
||||
xmlns="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing"
|
||||
targetNamespace="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing"
|
||||
elementFormDefault="qualified">
|
||||
<xsd:import namespace="http://schemas.openxmlformats.org/drawingml/2006/main"
|
||||
schemaLocation="dml-main.xsd"/>
|
||||
<xsd:import schemaLocation="wml.xsd"
|
||||
namespace="http://schemas.openxmlformats.org/wordprocessingml/2006/main"/>
|
||||
<xsd:import namespace="http://schemas.openxmlformats.org/drawingml/2006/picture"
|
||||
schemaLocation="dml-picture.xsd"/>
|
||||
<xsd:import namespace="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
|
||||
schemaLocation="shared-relationshipReference.xsd"/>
|
||||
<xsd:complexType name="CT_EffectExtent">
|
||||
<xsd:attribute name="l" type="a:ST_Coordinate" use="required"/>
|
||||
<xsd:attribute name="t" type="a:ST_Coordinate" use="required"/>
|
||||
<xsd:attribute name="r" type="a:ST_Coordinate" use="required"/>
|
||||
<xsd:attribute name="b" type="a:ST_Coordinate" use="required"/>
|
||||
</xsd:complexType>
|
||||
<xsd:simpleType name="ST_WrapDistance">
|
||||
<xsd:restriction base="xsd:unsignedInt"/>
|
||||
</xsd:simpleType>
|
||||
<xsd:complexType name="CT_Inline">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="extent" type="a:CT_PositiveSize2D"/>
|
||||
<xsd:element name="effectExtent" type="CT_EffectExtent" minOccurs="0"/>
|
||||
<xsd:element name="docPr" type="a:CT_NonVisualDrawingProps" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:element name="cNvGraphicFramePr" type="a:CT_NonVisualGraphicFrameProperties"
|
||||
minOccurs="0" maxOccurs="1"/>
|
||||
<xsd:element ref="a:graphic" minOccurs="1" maxOccurs="1"/>
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="distT" type="ST_WrapDistance" use="optional"/>
|
||||
<xsd:attribute name="distB" type="ST_WrapDistance" use="optional"/>
|
||||
<xsd:attribute name="distL" type="ST_WrapDistance" use="optional"/>
|
||||
<xsd:attribute name="distR" type="ST_WrapDistance" use="optional"/>
|
||||
</xsd:complexType>
|
||||
<xsd:simpleType name="ST_WrapText">
|
||||
<xsd:restriction base="xsd:token">
|
||||
<xsd:enumeration value="bothSides"/>
|
||||
<xsd:enumeration value="left"/>
|
||||
<xsd:enumeration value="right"/>
|
||||
<xsd:enumeration value="largest"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
<xsd:complexType name="CT_WrapPath">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="start" type="a:CT_Point2D" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:element name="lineTo" type="a:CT_Point2D" minOccurs="2" maxOccurs="unbounded"/>
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="edited" type="xsd:boolean" use="optional"/>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_WrapNone"/>
|
||||
<xsd:complexType name="CT_WrapSquare">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="effectExtent" type="CT_EffectExtent" minOccurs="0"/>
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="wrapText" type="ST_WrapText" use="required"/>
|
||||
<xsd:attribute name="distT" type="ST_WrapDistance" use="optional"/>
|
||||
<xsd:attribute name="distB" type="ST_WrapDistance" use="optional"/>
|
||||
<xsd:attribute name="distL" type="ST_WrapDistance" use="optional"/>
|
||||
<xsd:attribute name="distR" type="ST_WrapDistance" use="optional"/>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_WrapTight">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="wrapPolygon" type="CT_WrapPath" minOccurs="1" maxOccurs="1"/>
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="wrapText" type="ST_WrapText" use="required"/>
|
||||
<xsd:attribute name="distL" type="ST_WrapDistance" use="optional"/>
|
||||
<xsd:attribute name="distR" type="ST_WrapDistance" use="optional"/>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_WrapThrough">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="wrapPolygon" type="CT_WrapPath" minOccurs="1" maxOccurs="1"/>
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="wrapText" type="ST_WrapText" use="required"/>
|
||||
<xsd:attribute name="distL" type="ST_WrapDistance" use="optional"/>
|
||||
<xsd:attribute name="distR" type="ST_WrapDistance" use="optional"/>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_WrapTopBottom">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="effectExtent" type="CT_EffectExtent" minOccurs="0"/>
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="distT" type="ST_WrapDistance" use="optional"/>
|
||||
<xsd:attribute name="distB" type="ST_WrapDistance" use="optional"/>
|
||||
</xsd:complexType>
|
||||
<xsd:group name="EG_WrapType">
|
||||
<xsd:sequence>
|
||||
<xsd:choice minOccurs="1" maxOccurs="1">
|
||||
<xsd:element name="wrapNone" type="CT_WrapNone" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:element name="wrapSquare" type="CT_WrapSquare" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:element name="wrapTight" type="CT_WrapTight" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:element name="wrapThrough" type="CT_WrapThrough" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:element name="wrapTopAndBottom" type="CT_WrapTopBottom" minOccurs="1" maxOccurs="1"/>
|
||||
</xsd:choice>
|
||||
</xsd:sequence>
|
||||
</xsd:group>
|
||||
<xsd:simpleType name="ST_PositionOffset">
|
||||
<xsd:restriction base="xsd:int"/>
|
||||
</xsd:simpleType>
|
||||
<xsd:simpleType name="ST_AlignH">
|
||||
<xsd:restriction base="xsd:token">
|
||||
<xsd:enumeration value="left"/>
|
||||
<xsd:enumeration value="right"/>
|
||||
<xsd:enumeration value="center"/>
|
||||
<xsd:enumeration value="inside"/>
|
||||
<xsd:enumeration value="outside"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
<xsd:simpleType name="ST_RelFromH">
|
||||
<xsd:restriction base="xsd:token">
|
||||
<xsd:enumeration value="margin"/>
|
||||
<xsd:enumeration value="page"/>
|
||||
<xsd:enumeration value="column"/>
|
||||
<xsd:enumeration value="character"/>
|
||||
<xsd:enumeration value="leftMargin"/>
|
||||
<xsd:enumeration value="rightMargin"/>
|
||||
<xsd:enumeration value="insideMargin"/>
|
||||
<xsd:enumeration value="outsideMargin"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
<xsd:complexType name="CT_PosH">
|
||||
<xsd:sequence>
|
||||
<xsd:choice minOccurs="1" maxOccurs="1">
|
||||
<xsd:element name="align" type="ST_AlignH" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:element name="posOffset" type="ST_PositionOffset" minOccurs="1" maxOccurs="1"/>
|
||||
</xsd:choice>
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="relativeFrom" type="ST_RelFromH" use="required"/>
|
||||
</xsd:complexType>
|
||||
<xsd:simpleType name="ST_AlignV">
|
||||
<xsd:restriction base="xsd:token">
|
||||
<xsd:enumeration value="top"/>
|
||||
<xsd:enumeration value="bottom"/>
|
||||
<xsd:enumeration value="center"/>
|
||||
<xsd:enumeration value="inside"/>
|
||||
<xsd:enumeration value="outside"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
<xsd:simpleType name="ST_RelFromV">
|
||||
<xsd:restriction base="xsd:token">
|
||||
<xsd:enumeration value="margin"/>
|
||||
<xsd:enumeration value="page"/>
|
||||
<xsd:enumeration value="paragraph"/>
|
||||
<xsd:enumeration value="line"/>
|
||||
<xsd:enumeration value="topMargin"/>
|
||||
<xsd:enumeration value="bottomMargin"/>
|
||||
<xsd:enumeration value="insideMargin"/>
|
||||
<xsd:enumeration value="outsideMargin"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
<xsd:complexType name="CT_PosV">
|
||||
<xsd:sequence>
|
||||
<xsd:choice minOccurs="1" maxOccurs="1">
|
||||
<xsd:element name="align" type="ST_AlignV" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:element name="posOffset" type="ST_PositionOffset" minOccurs="1" maxOccurs="1"/>
|
||||
</xsd:choice>
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="relativeFrom" type="ST_RelFromV" use="required"/>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_Anchor">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="simplePos" type="a:CT_Point2D"/>
|
||||
<xsd:element name="positionH" type="CT_PosH"/>
|
||||
<xsd:element name="positionV" type="CT_PosV"/>
|
||||
<xsd:element name="extent" type="a:CT_PositiveSize2D"/>
|
||||
<xsd:element name="effectExtent" type="CT_EffectExtent" minOccurs="0"/>
|
||||
<xsd:group ref="EG_WrapType"/>
|
||||
<xsd:element name="docPr" type="a:CT_NonVisualDrawingProps" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:element name="cNvGraphicFramePr" type="a:CT_NonVisualGraphicFrameProperties"
|
||||
minOccurs="0" maxOccurs="1"/>
|
||||
<xsd:element ref="a:graphic" minOccurs="1" maxOccurs="1"/>
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="distT" type="ST_WrapDistance" use="optional"/>
|
||||
<xsd:attribute name="distB" type="ST_WrapDistance" use="optional"/>
|
||||
<xsd:attribute name="distL" type="ST_WrapDistance" use="optional"/>
|
||||
<xsd:attribute name="distR" type="ST_WrapDistance" use="optional"/>
|
||||
<xsd:attribute name="simplePos" type="xsd:boolean"/>
|
||||
<xsd:attribute name="relativeHeight" type="xsd:unsignedInt" use="required"/>
|
||||
<xsd:attribute name="behindDoc" type="xsd:boolean" use="required"/>
|
||||
<xsd:attribute name="locked" type="xsd:boolean" use="required"/>
|
||||
<xsd:attribute name="layoutInCell" type="xsd:boolean" use="required"/>
|
||||
<xsd:attribute name="hidden" type="xsd:boolean" use="optional"/>
|
||||
<xsd:attribute name="allowOverlap" type="xsd:boolean" use="required"/>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_TxbxContent">
|
||||
<xsd:group ref="w:EG_BlockLevelElts" minOccurs="1" maxOccurs="unbounded"/>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_TextboxInfo">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="txbxContent" type="CT_TxbxContent" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:element name="extLst" type="a:CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/>
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="id" type="xsd:unsignedShort" use="optional" default="0"/>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_LinkedTextboxInformation">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="extLst" type="a:CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/>
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="id" type="xsd:unsignedShort" use="required"/>
|
||||
<xsd:attribute name="seq" type="xsd:unsignedShort" use="required"/>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_WordprocessingShape">
|
||||
<xsd:sequence minOccurs="1" maxOccurs="1">
|
||||
<xsd:element name="cNvPr" type="a:CT_NonVisualDrawingProps" minOccurs="0" maxOccurs="1"/>
|
||||
<xsd:choice minOccurs="1" maxOccurs="1">
|
||||
<xsd:element name="cNvSpPr" type="a:CT_NonVisualDrawingShapeProps" minOccurs="1"
|
||||
maxOccurs="1"/>
|
||||
<xsd:element name="cNvCnPr" type="a:CT_NonVisualConnectorProperties" minOccurs="1"
|
||||
maxOccurs="1"/>
|
||||
</xsd:choice>
|
||||
<xsd:element name="spPr" type="a:CT_ShapeProperties" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:element name="style" type="a:CT_ShapeStyle" minOccurs="0" maxOccurs="1"/>
|
||||
<xsd:element name="extLst" type="a:CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/>
|
||||
<xsd:choice minOccurs="0" maxOccurs="1">
|
||||
<xsd:element name="txbx" type="CT_TextboxInfo" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:element name="linkedTxbx" type="CT_LinkedTextboxInformation" minOccurs="1"
|
||||
maxOccurs="1"/>
|
||||
</xsd:choice>
|
||||
<xsd:element name="bodyPr" type="a:CT_TextBodyProperties" minOccurs="1" maxOccurs="1"/>
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="normalEastAsianFlow" type="xsd:boolean" use="optional" default="false"/>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_GraphicFrame">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="cNvPr" type="a:CT_NonVisualDrawingProps" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:element name="cNvFrPr" type="a:CT_NonVisualGraphicFrameProperties" minOccurs="1"
|
||||
maxOccurs="1"/>
|
||||
<xsd:element name="xfrm" type="a:CT_Transform2D" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:element ref="a:graphic" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:element name="extLst" type="a:CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_WordprocessingContentPartNonVisual">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="cNvPr" type="a:CT_NonVisualDrawingProps" minOccurs="0" maxOccurs="1"/>
|
||||
<xsd:element name="cNvContentPartPr" type="a:CT_NonVisualContentPartProperties" minOccurs="0" maxOccurs="1"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_WordprocessingContentPart">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="nvContentPartPr" type="CT_WordprocessingContentPartNonVisual" minOccurs="0" maxOccurs="1"/>
|
||||
<xsd:element name="xfrm" type="a:CT_Transform2D" minOccurs="0" maxOccurs="1"/>
|
||||
<xsd:element name="extLst" type="a:CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/>
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="bwMode" type="a:ST_BlackWhiteMode" use="optional"/>
|
||||
<xsd:attribute ref="r:id" use="required"/>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_WordprocessingGroup">
|
||||
<xsd:sequence minOccurs="1" maxOccurs="1">
|
||||
<xsd:element name="cNvPr" type="a:CT_NonVisualDrawingProps" minOccurs="0" maxOccurs="1"/>
|
||||
<xsd:element name="cNvGrpSpPr" type="a:CT_NonVisualGroupDrawingShapeProps" minOccurs="1"
|
||||
maxOccurs="1"/>
|
||||
<xsd:element name="grpSpPr" type="a:CT_GroupShapeProperties" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:choice minOccurs="0" maxOccurs="unbounded">
|
||||
<xsd:element ref="wsp"/>
|
||||
<xsd:element name="grpSp" type="CT_WordprocessingGroup"/>
|
||||
<xsd:element name="graphicFrame" type="CT_GraphicFrame"/>
|
||||
<xsd:element ref="dpct:pic"/>
|
||||
<xsd:element name="contentPart" type="CT_WordprocessingContentPart"/>
|
||||
</xsd:choice>
|
||||
<xsd:element name="extLst" type="a:CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_WordprocessingCanvas">
|
||||
<xsd:sequence minOccurs="1" maxOccurs="1">
|
||||
<xsd:element name="bg" type="a:CT_BackgroundFormatting" minOccurs="0" maxOccurs="1"/>
|
||||
<xsd:element name="whole" type="a:CT_WholeE2oFormatting" minOccurs="0" maxOccurs="1"/>
|
||||
<xsd:choice minOccurs="0" maxOccurs="unbounded">
|
||||
<xsd:element ref="wsp"/>
|
||||
<xsd:element ref="dpct:pic"/>
|
||||
<xsd:element name="contentPart" type="CT_WordprocessingContentPart"/>
|
||||
<xsd:element ref="wgp"/>
|
||||
<xsd:element name="graphicFrame" type="CT_GraphicFrame"/>
|
||||
</xsd:choice>
|
||||
<xsd:element name="extLst" type="a:CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:element name="wpc" type="CT_WordprocessingCanvas"/>
|
||||
<xsd:element name="wgp" type="CT_WordprocessingGroup"/>
|
||||
<xsd:element name="wsp" type="CT_WordprocessingShape"/>
|
||||
<xsd:element name="inline" type="CT_Inline"/>
|
||||
<xsd:element name="anchor" type="CT_Anchor"/>
|
||||
</xsd:schema>
|
||||
+1676
File diff suppressed because it is too large.
Load diff
+28
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
|
||||
xmlns="http://schemas.openxmlformats.org/officeDocument/2006/characteristics"
|
||||
targetNamespace="http://schemas.openxmlformats.org/officeDocument/2006/characteristics"
|
||||
elementFormDefault="qualified">
|
||||
<xsd:complexType name="CT_AdditionalCharacteristics">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="characteristic" type="CT_Characteristic" minOccurs="0"
|
||||
maxOccurs="unbounded"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_Characteristic">
|
||||
<xsd:attribute name="name" type="xsd:string" use="required"/>
|
||||
<xsd:attribute name="relation" type="ST_Relation" use="required"/>
|
||||
<xsd:attribute name="val" type="xsd:string" use="required"/>
|
||||
<xsd:attribute name="vocabulary" type="xsd:anyURI" use="optional"/>
|
||||
</xsd:complexType>
|
||||
<xsd:simpleType name="ST_Relation">
|
||||
<xsd:restriction base="xsd:string">
|
||||
<xsd:enumeration value="ge"/>
|
||||
<xsd:enumeration value="le"/>
|
||||
<xsd:enumeration value="gt"/>
|
||||
<xsd:enumeration value="lt"/>
|
||||
<xsd:enumeration value="eq"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
<xsd:element name="additionalCharacteristics" type="CT_AdditionalCharacteristics"/>
|
||||
</xsd:schema>
|
||||
@@ -0,0 +1,144 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
|
||||
xmlns="http://schemas.openxmlformats.org/officeDocument/2006/bibliography"
|
||||
xmlns:s="http://schemas.openxmlformats.org/officeDocument/2006/sharedTypes"
|
||||
targetNamespace="http://schemas.openxmlformats.org/officeDocument/2006/bibliography"
|
||||
elementFormDefault="qualified">
|
||||
<xsd:import namespace="http://schemas.openxmlformats.org/officeDocument/2006/sharedTypes"
|
||||
schemaLocation="shared-commonSimpleTypes.xsd"/>
|
||||
<xsd:simpleType name="ST_SourceType">
|
||||
<xsd:restriction base="s:ST_String">
|
||||
<xsd:enumeration value="ArticleInAPeriodical"/>
|
||||
<xsd:enumeration value="Book"/>
|
||||
<xsd:enumeration value="BookSection"/>
|
||||
<xsd:enumeration value="JournalArticle"/>
|
||||
<xsd:enumeration value="ConferenceProceedings"/>
|
||||
<xsd:enumeration value="Report"/>
|
||||
<xsd:enumeration value="SoundRecording"/>
|
||||
<xsd:enumeration value="Performance"/>
|
||||
<xsd:enumeration value="Art"/>
|
||||
<xsd:enumeration value="DocumentFromInternetSite"/>
|
||||
<xsd:enumeration value="InternetSite"/>
|
||||
<xsd:enumeration value="Film"/>
|
||||
<xsd:enumeration value="Interview"/>
|
||||
<xsd:enumeration value="Patent"/>
|
||||
<xsd:enumeration value="ElectronicSource"/>
|
||||
<xsd:enumeration value="Case"/>
|
||||
<xsd:enumeration value="Misc"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
<xsd:complexType name="CT_NameListType">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="Person" type="CT_PersonType" minOccurs="1" maxOccurs="unbounded"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_PersonType">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="Last" type="s:ST_String" minOccurs="0" maxOccurs="unbounded"/>
|
||||
<xsd:element name="First" type="s:ST_String" minOccurs="0" maxOccurs="unbounded"/>
|
||||
<xsd:element name="Middle" type="s:ST_String" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_NameType">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="NameList" type="CT_NameListType" minOccurs="1" maxOccurs="1"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_NameOrCorporateType">
|
||||
<xsd:sequence>
|
||||
<xsd:choice minOccurs="0" maxOccurs="1">
|
||||
<xsd:element name="NameList" type="CT_NameListType" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:element name="Corporate" minOccurs="1" maxOccurs="1" type="s:ST_String"/>
|
||||
</xsd:choice>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_AuthorType">
|
||||
<xsd:sequence>
|
||||
<xsd:choice minOccurs="0" maxOccurs="unbounded">
|
||||
<xsd:element name="Artist" type="CT_NameType"/>
|
||||
<xsd:element name="Author" type="CT_NameOrCorporateType"/>
|
||||
<xsd:element name="BookAuthor" type="CT_NameType"/>
|
||||
<xsd:element name="Compiler" type="CT_NameType"/>
|
||||
<xsd:element name="Composer" type="CT_NameType"/>
|
||||
<xsd:element name="Conductor" type="CT_NameType"/>
|
||||
<xsd:element name="Counsel" type="CT_NameType"/>
|
||||
<xsd:element name="Director" type="CT_NameType"/>
|
||||
<xsd:element name="Editor" type="CT_NameType"/>
|
||||
<xsd:element name="Interviewee" type="CT_NameType"/>
|
||||
<xsd:element name="Interviewer" type="CT_NameType"/>
|
||||
<xsd:element name="Inventor" type="CT_NameType"/>
|
||||
<xsd:element name="Performer" type="CT_NameOrCorporateType"/>
|
||||
<xsd:element name="ProducerName" type="CT_NameType"/>
|
||||
<xsd:element name="Translator" type="CT_NameType"/>
|
||||
<xsd:element name="Writer" type="CT_NameType"/>
|
||||
</xsd:choice>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_SourceType">
|
||||
<xsd:sequence>
|
||||
<xsd:choice minOccurs="0" maxOccurs="unbounded">
|
||||
<xsd:element name="AbbreviatedCaseNumber" type="s:ST_String"/>
|
||||
<xsd:element name="AlbumTitle" type="s:ST_String"/>
|
||||
<xsd:element name="Author" type="CT_AuthorType"/>
|
||||
<xsd:element name="BookTitle" type="s:ST_String"/>
|
||||
<xsd:element name="Broadcaster" type="s:ST_String"/>
|
||||
<xsd:element name="BroadcastTitle" type="s:ST_String"/>
|
||||
<xsd:element name="CaseNumber" type="s:ST_String"/>
|
||||
<xsd:element name="ChapterNumber" type="s:ST_String"/>
|
||||
<xsd:element name="City" type="s:ST_String"/>
|
||||
<xsd:element name="Comments" type="s:ST_String"/>
|
||||
<xsd:element name="ConferenceName" type="s:ST_String"/>
|
||||
<xsd:element name="CountryRegion" type="s:ST_String"/>
|
||||
<xsd:element name="Court" type="s:ST_String"/>
|
||||
<xsd:element name="Day" type="s:ST_String"/>
|
||||
<xsd:element name="DayAccessed" type="s:ST_String"/>
|
||||
<xsd:element name="Department" type="s:ST_String"/>
|
||||
<xsd:element name="Distributor" type="s:ST_String"/>
|
||||
<xsd:element name="Edition" type="s:ST_String"/>
|
||||
<xsd:element name="Guid" type="s:ST_String"/>
|
||||
<xsd:element name="Institution" type="s:ST_String"/>
|
||||
<xsd:element name="InternetSiteTitle" type="s:ST_String"/>
|
||||
<xsd:element name="Issue" type="s:ST_String"/>
|
||||
<xsd:element name="JournalName" type="s:ST_String"/>
|
||||
<xsd:element name="LCID" type="s:ST_Lang"/>
|
||||
<xsd:element name="Medium" type="s:ST_String"/>
|
||||
<xsd:element name="Month" type="s:ST_String"/>
|
||||
<xsd:element name="MonthAccessed" type="s:ST_String"/>
|
||||
<xsd:element name="NumberVolumes" type="s:ST_String"/>
|
||||
<xsd:element name="Pages" type="s:ST_String"/>
|
||||
<xsd:element name="PatentNumber" type="s:ST_String"/>
|
||||
<xsd:element name="PeriodicalTitle" type="s:ST_String"/>
|
||||
<xsd:element name="ProductionCompany" type="s:ST_String"/>
|
||||
<xsd:element name="PublicationTitle" type="s:ST_String"/>
|
||||
<xsd:element name="Publisher" type="s:ST_String"/>
|
||||
<xsd:element name="RecordingNumber" type="s:ST_String"/>
|
||||
<xsd:element name="RefOrder" type="s:ST_String"/>
|
||||
<xsd:element name="Reporter" type="s:ST_String"/>
|
||||
<xsd:element name="SourceType" type="ST_SourceType"/>
|
||||
<xsd:element name="ShortTitle" type="s:ST_String"/>
|
||||
<xsd:element name="StandardNumber" type="s:ST_String"/>
|
||||
<xsd:element name="StateProvince" type="s:ST_String"/>
|
||||
<xsd:element name="Station" type="s:ST_String"/>
|
||||
<xsd:element name="Tag" type="s:ST_String"/>
|
||||
<xsd:element name="Theater" type="s:ST_String"/>
|
||||
<xsd:element name="ThesisType" type="s:ST_String"/>
|
||||
<xsd:element name="Title" type="s:ST_String"/>
|
||||
<xsd:element name="Type" type="s:ST_String"/>
|
||||
<xsd:element name="URL" type="s:ST_String"/>
|
||||
<xsd:element name="Version" type="s:ST_String"/>
|
||||
<xsd:element name="Volume" type="s:ST_String"/>
|
||||
<xsd:element name="Year" type="s:ST_String"/>
|
||||
<xsd:element name="YearAccessed" type="s:ST_String"/>
|
||||
</xsd:choice>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:element name="Sources" type="CT_Sources"/>
|
||||
<xsd:complexType name="CT_Sources">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="Source" type="CT_SourceType" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="SelectedStyle" type="s:ST_String"/>
|
||||
<xsd:attribute name="StyleName" type="s:ST_String"/>
|
||||
<xsd:attribute name="URI" type="s:ST_String"/>
|
||||
</xsd:complexType>
|
||||
</xsd:schema>
|
||||
+174
@@ -0,0 +1,174 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
|
||||
xmlns="http://schemas.openxmlformats.org/officeDocument/2006/sharedTypes"
|
||||
targetNamespace="http://schemas.openxmlformats.org/officeDocument/2006/sharedTypes"
|
||||
elementFormDefault="qualified">
|
||||
<xsd:simpleType name="ST_Lang">
|
||||
<xsd:restriction base="xsd:string"/>
|
||||
</xsd:simpleType>
|
||||
<xsd:simpleType name="ST_HexColorRGB">
|
||||
<xsd:restriction base="xsd:hexBinary">
|
||||
<xsd:length value="3" fixed="true"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
<xsd:simpleType name="ST_Panose">
|
||||
<xsd:restriction base="xsd:hexBinary">
|
||||
<xsd:length value="10"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
<xsd:simpleType name="ST_CalendarType">
|
||||
<xsd:restriction base="xsd:string">
|
||||
<xsd:enumeration value="gregorian"/>
|
||||
<xsd:enumeration value="gregorianUs"/>
|
||||
<xsd:enumeration value="gregorianMeFrench"/>
|
||||
<xsd:enumeration value="gregorianArabic"/>
|
||||
<xsd:enumeration value="hijri"/>
|
||||
<xsd:enumeration value="hebrew"/>
|
||||
<xsd:enumeration value="taiwan"/>
|
||||
<xsd:enumeration value="japan"/>
|
||||
<xsd:enumeration value="thai"/>
|
||||
<xsd:enumeration value="korea"/>
|
||||
<xsd:enumeration value="saka"/>
|
||||
<xsd:enumeration value="gregorianXlitEnglish"/>
|
||||
<xsd:enumeration value="gregorianXlitFrench"/>
|
||||
<xsd:enumeration value="none"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
<xsd:simpleType name="ST_AlgClass">
|
||||
<xsd:restriction base="xsd:string">
|
||||
<xsd:enumeration value="hash"/>
|
||||
<xsd:enumeration value="custom"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
<xsd:simpleType name="ST_CryptProv">
|
||||
<xsd:restriction base="xsd:string">
|
||||
<xsd:enumeration value="rsaAES"/>
|
||||
<xsd:enumeration value="rsaFull"/>
|
||||
<xsd:enumeration value="custom"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
<xsd:simpleType name="ST_AlgType">
|
||||
<xsd:restriction base="xsd:string">
|
||||
<xsd:enumeration value="typeAny"/>
|
||||
<xsd:enumeration value="custom"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
<xsd:simpleType name="ST_ColorType">
|
||||
<xsd:restriction base="xsd:string"/>
|
||||
</xsd:simpleType>
|
||||
<xsd:simpleType name="ST_Guid">
|
||||
<xsd:restriction base="xsd:token">
|
||||
<xsd:pattern value="\{[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}\}"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
<xsd:simpleType name="ST_OnOff">
|
||||
<xsd:union memberTypes="xsd:boolean ST_OnOff1"/>
|
||||
</xsd:simpleType>
|
||||
<xsd:simpleType name="ST_OnOff1">
|
||||
<xsd:restriction base="xsd:string">
|
||||
<xsd:enumeration value="on"/>
|
||||
<xsd:enumeration value="off"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
<xsd:simpleType name="ST_String">
|
||||
<xsd:restriction base="xsd:string"/>
|
||||
</xsd:simpleType>
|
||||
<xsd:simpleType name="ST_XmlName">
|
||||
<xsd:restriction base="xsd:NCName">
|
||||
<xsd:minLength value="1"/>
|
||||
<xsd:maxLength value="255"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
<xsd:simpleType name="ST_TrueFalse">
|
||||
<xsd:restriction base="xsd:string">
|
||||
<xsd:enumeration value="t"/>
|
||||
<xsd:enumeration value="f"/>
|
||||
<xsd:enumeration value="true"/>
|
||||
<xsd:enumeration value="false"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
<xsd:simpleType name="ST_TrueFalseBlank">
|
||||
<xsd:restriction base="xsd:string">
|
||||
<xsd:enumeration value="t"/>
|
||||
<xsd:enumeration value="f"/>
|
||||
<xsd:enumeration value="true"/>
|
||||
<xsd:enumeration value="false"/>
|
||||
<xsd:enumeration value=""/>
|
||||
<xsd:enumeration value="True"/>
|
||||
<xsd:enumeration value="False"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
<xsd:simpleType name="ST_UnsignedDecimalNumber">
|
||||
<xsd:restriction base="xsd:decimal">
|
||||
<xsd:minInclusive value="0"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
<xsd:simpleType name="ST_TwipsMeasure">
|
||||
<xsd:union memberTypes="ST_UnsignedDecimalNumber ST_PositiveUniversalMeasure"/>
|
||||
</xsd:simpleType>
|
||||
<xsd:simpleType name="ST_VerticalAlignRun">
|
||||
<xsd:restriction base="xsd:string">
|
||||
<xsd:enumeration value="baseline"/>
|
||||
<xsd:enumeration value="superscript"/>
|
||||
<xsd:enumeration value="subscript"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
<xsd:simpleType name="ST_Xstring">
|
||||
<xsd:restriction base="xsd:string"/>
|
||||
</xsd:simpleType>
|
||||
<xsd:simpleType name="ST_XAlign">
|
||||
<xsd:restriction base="xsd:string">
|
||||
<xsd:enumeration value="left"/>
|
||||
<xsd:enumeration value="center"/>
|
||||
<xsd:enumeration value="right"/>
|
||||
<xsd:enumeration value="inside"/>
|
||||
<xsd:enumeration value="outside"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
<xsd:simpleType name="ST_YAlign">
|
||||
<xsd:restriction base="xsd:string">
|
||||
<xsd:enumeration value="inline"/>
|
||||
<xsd:enumeration value="top"/>
|
||||
<xsd:enumeration value="center"/>
|
||||
<xsd:enumeration value="bottom"/>
|
||||
<xsd:enumeration value="inside"/>
|
||||
<xsd:enumeration value="outside"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
<xsd:simpleType name="ST_ConformanceClass">
|
||||
<xsd:restriction base="xsd:string">
|
||||
<xsd:enumeration value="strict"/>
|
||||
<xsd:enumeration value="transitional"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
<xsd:simpleType name="ST_UniversalMeasure">
|
||||
<xsd:restriction base="xsd:string">
|
||||
<xsd:pattern value="-?[0-9]+(\.[0-9]+)?(mm|cm|in|pt|pc|pi)"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
<xsd:simpleType name="ST_PositiveUniversalMeasure">
|
||||
<xsd:restriction base="ST_UniversalMeasure">
|
||||
<xsd:pattern value="[0-9]+(\.[0-9]+)?(mm|cm|in|pt|pc|pi)"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
<xsd:simpleType name="ST_Percentage">
|
||||
<xsd:restriction base="xsd:string">
|
||||
<xsd:pattern value="-?[0-9]+(\.[0-9]+)?%"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
<xsd:simpleType name="ST_FixedPercentage">
|
||||
<xsd:restriction base="ST_Percentage">
|
||||
<xsd:pattern value="-?((100)|([0-9][0-9]?))(\.[0-9][0-9]?)?%"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
<xsd:simpleType name="ST_PositivePercentage">
|
||||
<xsd:restriction base="ST_Percentage">
|
||||
<xsd:pattern value="[0-9]+(\.[0-9]+)?%"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
<xsd:simpleType name="ST_PositiveFixedPercentage">
|
||||
<xsd:restriction base="ST_Percentage">
|
||||
<xsd:pattern value="((100)|([0-9][0-9]?))(\.[0-9][0-9]?)?%"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
</xsd:schema>
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
|
||||
xmlns="http://schemas.openxmlformats.org/officeDocument/2006/customXml"
|
||||
xmlns:s="http://schemas.openxmlformats.org/officeDocument/2006/sharedTypes"
|
||||
targetNamespace="http://schemas.openxmlformats.org/officeDocument/2006/customXml"
|
||||
elementFormDefault="qualified" attributeFormDefault="qualified" blockDefault="#all">
|
||||
<xsd:import namespace="http://schemas.openxmlformats.org/officeDocument/2006/sharedTypes"
|
||||
schemaLocation="shared-commonSimpleTypes.xsd"/>
|
||||
<xsd:complexType name="CT_DatastoreSchemaRef">
|
||||
<xsd:attribute name="uri" type="xsd:string" use="required"/>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_DatastoreSchemaRefs">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="schemaRef" type="CT_DatastoreSchemaRef" minOccurs="0" maxOccurs="unbounded"
|
||||
/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_DatastoreItem">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="schemaRefs" type="CT_DatastoreSchemaRefs" minOccurs="0"/>
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="itemID" type="s:ST_Guid" use="required"/>
|
||||
</xsd:complexType>
|
||||
<xsd:element name="datastoreItem" type="CT_DatastoreItem"/>
|
||||
</xsd:schema>
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
|
||||
xmlns="http://schemas.openxmlformats.org/schemaLibrary/2006/main"
|
||||
targetNamespace="http://schemas.openxmlformats.org/schemaLibrary/2006/main"
|
||||
attributeFormDefault="qualified" elementFormDefault="qualified">
|
||||
<xsd:complexType name="CT_Schema">
|
||||
<xsd:attribute name="uri" type="xsd:string" default=""/>
|
||||
<xsd:attribute name="manifestLocation" type="xsd:string"/>
|
||||
<xsd:attribute name="schemaLocation" type="xsd:string"/>
|
||||
<xsd:attribute name="schemaLanguage" type="xsd:token"/>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_SchemaLibrary">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="schema" type="CT_Schema" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:element name="schemaLibrary" type="CT_SchemaLibrary"/>
|
||||
</xsd:schema>
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
|
||||
xmlns="http://schemas.openxmlformats.org/officeDocument/2006/custom-properties"
|
||||
xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes"
|
||||
xmlns:s="http://schemas.openxmlformats.org/officeDocument/2006/sharedTypes"
|
||||
targetNamespace="http://schemas.openxmlformats.org/officeDocument/2006/custom-properties"
|
||||
blockDefault="#all" elementFormDefault="qualified">
|
||||
<xsd:import namespace="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes"
|
||||
schemaLocation="shared-documentPropertiesVariantTypes.xsd"/>
|
||||
<xsd:import namespace="http://schemas.openxmlformats.org/officeDocument/2006/sharedTypes"
|
||||
schemaLocation="shared-commonSimpleTypes.xsd"/>
|
||||
<xsd:element name="Properties" type="CT_Properties"/>
|
||||
<xsd:complexType name="CT_Properties">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="property" minOccurs="0" maxOccurs="unbounded" type="CT_Property"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_Property">
|
||||
<xsd:choice minOccurs="1" maxOccurs="1">
|
||||
<xsd:element ref="vt:vector"/>
|
||||
<xsd:element ref="vt:array"/>
|
||||
<xsd:element ref="vt:blob"/>
|
||||
<xsd:element ref="vt:oblob"/>
|
||||
<xsd:element ref="vt:empty"/>
|
||||
<xsd:element ref="vt:null"/>
|
||||
<xsd:element ref="vt:i1"/>
|
||||
<xsd:element ref="vt:i2"/>
|
||||
<xsd:element ref="vt:i4"/>
|
||||
<xsd:element ref="vt:i8"/>
|
||||
<xsd:element ref="vt:int"/>
|
||||
<xsd:element ref="vt:ui1"/>
|
||||
<xsd:element ref="vt:ui2"/>
|
||||
<xsd:element ref="vt:ui4"/>
|
||||
<xsd:element ref="vt:ui8"/>
|
||||
<xsd:element ref="vt:uint"/>
|
||||
<xsd:element ref="vt:r4"/>
|
||||
<xsd:element ref="vt:r8"/>
|
||||
<xsd:element ref="vt:decimal"/>
|
||||
<xsd:element ref="vt:lpstr"/>
|
||||
<xsd:element ref="vt:lpwstr"/>
|
||||
<xsd:element ref="vt:bstr"/>
|
||||
<xsd:element ref="vt:date"/>
|
||||
<xsd:element ref="vt:filetime"/>
|
||||
<xsd:element ref="vt:bool"/>
|
||||
<xsd:element ref="vt:cy"/>
|
||||
<xsd:element ref="vt:error"/>
|
||||
<xsd:element ref="vt:stream"/>
|
||||
<xsd:element ref="vt:ostream"/>
|
||||
<xsd:element ref="vt:storage"/>
|
||||
<xsd:element ref="vt:ostorage"/>
|
||||
<xsd:element ref="vt:vstream"/>
|
||||
<xsd:element ref="vt:clsid"/>
|
||||
</xsd:choice>
|
||||
<xsd:attribute name="fmtid" use="required" type="s:ST_Guid"/>
|
||||
<xsd:attribute name="pid" use="required" type="xsd:int"/>
|
||||
<xsd:attribute name="name" use="optional" type="xsd:string"/>
|
||||
<xsd:attribute name="linkTarget" use="optional" type="xsd:string"/>
|
||||
</xsd:complexType>
|
||||
</xsd:schema>
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
|
||||
xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties"
|
||||
xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes"
|
||||
targetNamespace="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties"
|
||||
elementFormDefault="qualified" blockDefault="#all">
|
||||
<xsd:import namespace="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes"
|
||||
schemaLocation="shared-documentPropertiesVariantTypes.xsd"/>
|
||||
<xsd:element name="Properties" type="CT_Properties"/>
|
||||
<xsd:complexType name="CT_Properties">
|
||||
<xsd:all>
|
||||
<xsd:element name="Template" minOccurs="0" maxOccurs="1" type="xsd:string"/>
|
||||
<xsd:element name="Manager" minOccurs="0" maxOccurs="1" type="xsd:string"/>
|
||||
<xsd:element name="Company" minOccurs="0" maxOccurs="1" type="xsd:string"/>
|
||||
<xsd:element name="Pages" minOccurs="0" maxOccurs="1" type="xsd:int"/>
|
||||
<xsd:element name="Words" minOccurs="0" maxOccurs="1" type="xsd:int"/>
|
||||
<xsd:element name="Characters" minOccurs="0" maxOccurs="1" type="xsd:int"/>
|
||||
<xsd:element name="PresentationFormat" minOccurs="0" maxOccurs="1" type="xsd:string"/>
|
||||
<xsd:element name="Lines" minOccurs="0" maxOccurs="1" type="xsd:int"/>
|
||||
<xsd:element name="Paragraphs" minOccurs="0" maxOccurs="1" type="xsd:int"/>
|
||||
<xsd:element name="Slides" minOccurs="0" maxOccurs="1" type="xsd:int"/>
|
||||
<xsd:element name="Notes" minOccurs="0" maxOccurs="1" type="xsd:int"/>
|
||||
<xsd:element name="TotalTime" minOccurs="0" maxOccurs="1" type="xsd:int"/>
|
||||
<xsd:element name="HiddenSlides" minOccurs="0" maxOccurs="1" type="xsd:int"/>
|
||||
<xsd:element name="MMClips" minOccurs="0" maxOccurs="1" type="xsd:int"/>
|
||||
<xsd:element name="ScaleCrop" minOccurs="0" maxOccurs="1" type="xsd:boolean"/>
|
||||
<xsd:element name="HeadingPairs" minOccurs="0" maxOccurs="1" type="CT_VectorVariant"/>
|
||||
<xsd:element name="TitlesOfParts" minOccurs="0" maxOccurs="1" type="CT_VectorLpstr"/>
|
||||
<xsd:element name="LinksUpToDate" minOccurs="0" maxOccurs="1" type="xsd:boolean"/>
|
||||
<xsd:element name="CharactersWithSpaces" minOccurs="0" maxOccurs="1" type="xsd:int"/>
|
||||
<xsd:element name="SharedDoc" minOccurs="0" maxOccurs="1" type="xsd:boolean"/>
|
||||
<xsd:element name="HyperlinkBase" minOccurs="0" maxOccurs="1" type="xsd:string"/>
|
||||
<xsd:element name="HLinks" minOccurs="0" maxOccurs="1" type="CT_VectorVariant"/>
|
||||
<xsd:element name="HyperlinksChanged" minOccurs="0" maxOccurs="1" type="xsd:boolean"/>
|
||||
<xsd:element name="DigSig" minOccurs="0" maxOccurs="1" type="CT_DigSigBlob"/>
|
||||
<xsd:element name="Application" minOccurs="0" maxOccurs="1" type="xsd:string"/>
|
||||
<xsd:element name="AppVersion" minOccurs="0" maxOccurs="1" type="xsd:string"/>
|
||||
<xsd:element name="DocSecurity" minOccurs="0" maxOccurs="1" type="xsd:int"/>
|
||||
</xsd:all>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_VectorVariant">
|
||||
<xsd:sequence minOccurs="1" maxOccurs="1">
|
||||
<xsd:element ref="vt:vector"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_VectorLpstr">
|
||||
<xsd:sequence minOccurs="1" maxOccurs="1">
|
||||
<xsd:element ref="vt:vector"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_DigSigBlob">
|
||||
<xsd:sequence minOccurs="1" maxOccurs="1">
|
||||
<xsd:element ref="vt:blob"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
</xsd:schema>
|
||||
+195
@@ -0,0 +1,195 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
|
||||
xmlns="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes"
|
||||
xmlns:s="http://schemas.openxmlformats.org/officeDocument/2006/sharedTypes"
|
||||
targetNamespace="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes"
|
||||
blockDefault="#all" elementFormDefault="qualified">
|
||||
<xsd:import namespace="http://schemas.openxmlformats.org/officeDocument/2006/sharedTypes"
|
||||
schemaLocation="shared-commonSimpleTypes.xsd"/>
|
||||
<xsd:simpleType name="ST_VectorBaseType">
|
||||
<xsd:restriction base="xsd:string">
|
||||
<xsd:enumeration value="variant"/>
|
||||
<xsd:enumeration value="i1"/>
|
||||
<xsd:enumeration value="i2"/>
|
||||
<xsd:enumeration value="i4"/>
|
||||
<xsd:enumeration value="i8"/>
|
||||
<xsd:enumeration value="ui1"/>
|
||||
<xsd:enumeration value="ui2"/>
|
||||
<xsd:enumeration value="ui4"/>
|
||||
<xsd:enumeration value="ui8"/>
|
||||
<xsd:enumeration value="r4"/>
|
||||
<xsd:enumeration value="r8"/>
|
||||
<xsd:enumeration value="lpstr"/>
|
||||
<xsd:enumeration value="lpwstr"/>
|
||||
<xsd:enumeration value="bstr"/>
|
||||
<xsd:enumeration value="date"/>
|
||||
<xsd:enumeration value="filetime"/>
|
||||
<xsd:enumeration value="bool"/>
|
||||
<xsd:enumeration value="cy"/>
|
||||
<xsd:enumeration value="error"/>
|
||||
<xsd:enumeration value="clsid"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
<xsd:simpleType name="ST_ArrayBaseType">
|
||||
<xsd:restriction base="xsd:string">
|
||||
<xsd:enumeration value="variant"/>
|
||||
<xsd:enumeration value="i1"/>
|
||||
<xsd:enumeration value="i2"/>
|
||||
<xsd:enumeration value="i4"/>
|
||||
<xsd:enumeration value="int"/>
|
||||
<xsd:enumeration value="ui1"/>
|
||||
<xsd:enumeration value="ui2"/>
|
||||
<xsd:enumeration value="ui4"/>
|
||||
<xsd:enumeration value="uint"/>
|
||||
<xsd:enumeration value="r4"/>
|
||||
<xsd:enumeration value="r8"/>
|
||||
<xsd:enumeration value="decimal"/>
|
||||
<xsd:enumeration value="bstr"/>
|
||||
<xsd:enumeration value="date"/>
|
||||
<xsd:enumeration value="bool"/>
|
||||
<xsd:enumeration value="cy"/>
|
||||
<xsd:enumeration value="error"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
<xsd:simpleType name="ST_Cy">
|
||||
<xsd:restriction base="xsd:string">
|
||||
<xsd:pattern value="\s*[0-9]*\.[0-9]{4}\s*"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
<xsd:simpleType name="ST_Error">
|
||||
<xsd:restriction base="xsd:string">
|
||||
<xsd:pattern value="\s*0x[0-9A-Za-z]{8}\s*"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
<xsd:complexType name="CT_Empty"/>
|
||||
<xsd:complexType name="CT_Null"/>
|
||||
<xsd:complexType name="CT_Vector">
|
||||
<xsd:choice minOccurs="1" maxOccurs="unbounded">
|
||||
<xsd:element ref="variant"/>
|
||||
<xsd:element ref="i1"/>
|
||||
<xsd:element ref="i2"/>
|
||||
<xsd:element ref="i4"/>
|
||||
<xsd:element ref="i8"/>
|
||||
<xsd:element ref="ui1"/>
|
||||
<xsd:element ref="ui2"/>
|
||||
<xsd:element ref="ui4"/>
|
||||
<xsd:element ref="ui8"/>
|
||||
<xsd:element ref="r4"/>
|
||||
<xsd:element ref="r8"/>
|
||||
<xsd:element ref="lpstr"/>
|
||||
<xsd:element ref="lpwstr"/>
|
||||
<xsd:element ref="bstr"/>
|
||||
<xsd:element ref="date"/>
|
||||
<xsd:element ref="filetime"/>
|
||||
<xsd:element ref="bool"/>
|
||||
<xsd:element ref="cy"/>
|
||||
<xsd:element ref="error"/>
|
||||
<xsd:element ref="clsid"/>
|
||||
</xsd:choice>
|
||||
<xsd:attribute name="baseType" type="ST_VectorBaseType" use="required"/>
|
||||
<xsd:attribute name="size" type="xsd:unsignedInt" use="required"/>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_Array">
|
||||
<xsd:choice minOccurs="1" maxOccurs="unbounded">
|
||||
<xsd:element ref="variant"/>
|
||||
<xsd:element ref="i1"/>
|
||||
<xsd:element ref="i2"/>
|
||||
<xsd:element ref="i4"/>
|
||||
<xsd:element ref="int"/>
|
||||
<xsd:element ref="ui1"/>
|
||||
<xsd:element ref="ui2"/>
|
||||
<xsd:element ref="ui4"/>
|
||||
<xsd:element ref="uint"/>
|
||||
<xsd:element ref="r4"/>
|
||||
<xsd:element ref="r8"/>
|
||||
<xsd:element ref="decimal"/>
|
||||
<xsd:element ref="bstr"/>
|
||||
<xsd:element ref="date"/>
|
||||
<xsd:element ref="bool"/>
|
||||
<xsd:element ref="error"/>
|
||||
<xsd:element ref="cy"/>
|
||||
</xsd:choice>
|
||||
<xsd:attribute name="lBounds" type="xsd:int" use="required"/>
|
||||
<xsd:attribute name="uBounds" type="xsd:int" use="required"/>
|
||||
<xsd:attribute name="baseType" type="ST_ArrayBaseType" use="required"/>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_Variant">
|
||||
<xsd:choice minOccurs="1" maxOccurs="1">
|
||||
<xsd:element ref="variant"/>
|
||||
<xsd:element ref="vector"/>
|
||||
<xsd:element ref="array"/>
|
||||
<xsd:element ref="blob"/>
|
||||
<xsd:element ref="oblob"/>
|
||||
<xsd:element ref="empty"/>
|
||||
<xsd:element ref="null"/>
|
||||
<xsd:element ref="i1"/>
|
||||
<xsd:element ref="i2"/>
|
||||
<xsd:element ref="i4"/>
|
||||
<xsd:element ref="i8"/>
|
||||
<xsd:element ref="int"/>
|
||||
<xsd:element ref="ui1"/>
|
||||
<xsd:element ref="ui2"/>
|
||||
<xsd:element ref="ui4"/>
|
||||
<xsd:element ref="ui8"/>
|
||||
<xsd:element ref="uint"/>
|
||||
<xsd:element ref="r4"/>
|
||||
<xsd:element ref="r8"/>
|
||||
<xsd:element ref="decimal"/>
|
||||
<xsd:element ref="lpstr"/>
|
||||
<xsd:element ref="lpwstr"/>
|
||||
<xsd:element ref="bstr"/>
|
||||
<xsd:element ref="date"/>
|
||||
<xsd:element ref="filetime"/>
|
||||
<xsd:element ref="bool"/>
|
||||
<xsd:element ref="cy"/>
|
||||
<xsd:element ref="error"/>
|
||||
<xsd:element ref="stream"/>
|
||||
<xsd:element ref="ostream"/>
|
||||
<xsd:element ref="storage"/>
|
||||
<xsd:element ref="ostorage"/>
|
||||
<xsd:element ref="vstream"/>
|
||||
<xsd:element ref="clsid"/>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_Vstream">
|
||||
<xsd:simpleContent>
|
||||
<xsd:extension base="xsd:base64Binary">
|
||||
<xsd:attribute name="version" type="s:ST_Guid"/>
|
||||
</xsd:extension>
|
||||
</xsd:simpleContent>
|
||||
</xsd:complexType>
|
||||
<xsd:element name="variant" type="CT_Variant"/>
|
||||
<xsd:element name="vector" type="CT_Vector"/>
|
||||
<xsd:element name="array" type="CT_Array"/>
|
||||
<xsd:element name="blob" type="xsd:base64Binary"/>
|
||||
<xsd:element name="oblob" type="xsd:base64Binary"/>
|
||||
<xsd:element name="empty" type="CT_Empty"/>
|
||||
<xsd:element name="null" type="CT_Null"/>
|
||||
<xsd:element name="i1" type="xsd:byte"/>
|
||||
<xsd:element name="i2" type="xsd:short"/>
|
||||
<xsd:element name="i4" type="xsd:int"/>
|
||||
<xsd:element name="i8" type="xsd:long"/>
|
||||
<xsd:element name="int" type="xsd:int"/>
|
||||
<xsd:element name="ui1" type="xsd:unsignedByte"/>
|
||||
<xsd:element name="ui2" type="xsd:unsignedShort"/>
|
||||
<xsd:element name="ui4" type="xsd:unsignedInt"/>
|
||||
<xsd:element name="ui8" type="xsd:unsignedLong"/>
|
||||
<xsd:element name="uint" type="xsd:unsignedInt"/>
|
||||
<xsd:element name="r4" type="xsd:float"/>
|
||||
<xsd:element name="r8" type="xsd:double"/>
|
||||
<xsd:element name="decimal" type="xsd:decimal"/>
|
||||
<xsd:element name="lpstr" type="xsd:string"/>
|
||||
<xsd:element name="lpwstr" type="xsd:string"/>
|
||||
<xsd:element name="bstr" type="xsd:string"/>
|
||||
<xsd:element name="date" type="xsd:dateTime"/>
|
||||
<xsd:element name="filetime" type="xsd:dateTime"/>
|
||||
<xsd:element name="bool" type="xsd:boolean"/>
|
||||
<xsd:element name="cy" type="ST_Cy"/>
|
||||
<xsd:element name="error" type="ST_Error"/>
|
||||
<xsd:element name="stream" type="xsd:base64Binary"/>
|
||||
<xsd:element name="ostream" type="xsd:base64Binary"/>
|
||||
<xsd:element name="storage" type="xsd:base64Binary"/>
|
||||
<xsd:element name="ostorage" type="xsd:base64Binary"/>
|
||||
<xsd:element name="vstream" type="CT_Vstream"/>
|
||||
<xsd:element name="clsid" type="s:ST_Guid"/>
|
||||
</xsd:schema>
|
||||
@@ -0,0 +1,582 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
|
||||
xmlns="http://schemas.openxmlformats.org/officeDocument/2006/math"
|
||||
xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"
|
||||
xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
|
||||
xmlns:s="http://schemas.openxmlformats.org/officeDocument/2006/sharedTypes"
|
||||
elementFormDefault="qualified" attributeFormDefault="qualified" blockDefault="#all"
|
||||
targetNamespace="http://schemas.openxmlformats.org/officeDocument/2006/math">
|
||||
<xsd:import namespace="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
|
||||
schemaLocation="wml.xsd"/>
|
||||
<xsd:import namespace="http://schemas.openxmlformats.org/officeDocument/2006/sharedTypes"
|
||||
schemaLocation="shared-commonSimpleTypes.xsd"/>
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" schemaLocation="xml.xsd"/>
|
||||
<xsd:simpleType name="ST_Integer255">
|
||||
<xsd:restriction base="xsd:integer">
|
||||
<xsd:minInclusive value="1"/>
|
||||
<xsd:maxInclusive value="255"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
<xsd:complexType name="CT_Integer255">
|
||||
<xsd:attribute name="val" type="ST_Integer255" use="required"/>
|
||||
</xsd:complexType>
|
||||
<xsd:simpleType name="ST_Integer2">
|
||||
<xsd:restriction base="xsd:integer">
|
||||
<xsd:minInclusive value="-2"/>
|
||||
<xsd:maxInclusive value="2"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
<xsd:complexType name="CT_Integer2">
|
||||
<xsd:attribute name="val" type="ST_Integer2" use="required"/>
|
||||
</xsd:complexType>
|
||||
<xsd:simpleType name="ST_SpacingRule">
|
||||
<xsd:restriction base="xsd:integer">
|
||||
<xsd:minInclusive value="0"/>
|
||||
<xsd:maxInclusive value="4"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
<xsd:complexType name="CT_SpacingRule">
|
||||
<xsd:attribute name="val" type="ST_SpacingRule" use="required"/>
|
||||
</xsd:complexType>
|
||||
<xsd:simpleType name="ST_UnSignedInteger">
|
||||
<xsd:restriction base="xsd:unsignedInt"/>
|
||||
</xsd:simpleType>
|
||||
<xsd:complexType name="CT_UnSignedInteger">
|
||||
<xsd:attribute name="val" type="ST_UnSignedInteger" use="required"/>
|
||||
</xsd:complexType>
|
||||
<xsd:simpleType name="ST_Char">
|
||||
<xsd:restriction base="xsd:string">
|
||||
<xsd:maxLength value="1"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
<xsd:complexType name="CT_Char">
|
||||
<xsd:attribute name="val" type="ST_Char" use="required"/>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_OnOff">
|
||||
<xsd:attribute name="val" type="s:ST_OnOff"/>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_String">
|
||||
<xsd:attribute name="val" type="s:ST_String"/>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_XAlign">
|
||||
<xsd:attribute name="val" type="s:ST_XAlign" use="required"/>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_YAlign">
|
||||
<xsd:attribute name="val" type="s:ST_YAlign" use="required"/>
|
||||
</xsd:complexType>
|
||||
<xsd:simpleType name="ST_Shp">
|
||||
<xsd:restriction base="xsd:string">
|
||||
<xsd:enumeration value="centered"/>
|
||||
<xsd:enumeration value="match"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
<xsd:complexType name="CT_Shp">
|
||||
<xsd:attribute name="val" type="ST_Shp" use="required"/>
|
||||
</xsd:complexType>
|
||||
<xsd:simpleType name="ST_FType">
|
||||
<xsd:restriction base="xsd:string">
|
||||
<xsd:enumeration value="bar"/>
|
||||
<xsd:enumeration value="skw"/>
|
||||
<xsd:enumeration value="lin"/>
|
||||
<xsd:enumeration value="noBar"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
<xsd:complexType name="CT_FType">
|
||||
<xsd:attribute name="val" type="ST_FType" use="required"/>
|
||||
</xsd:complexType>
|
||||
<xsd:simpleType name="ST_LimLoc">
|
||||
<xsd:restriction base="xsd:string">
|
||||
<xsd:enumeration value="undOvr"/>
|
||||
<xsd:enumeration value="subSup"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
<xsd:complexType name="CT_LimLoc">
|
||||
<xsd:attribute name="val" type="ST_LimLoc" use="required"/>
|
||||
</xsd:complexType>
|
||||
<xsd:simpleType name="ST_TopBot">
|
||||
<xsd:restriction base="xsd:string">
|
||||
<xsd:enumeration value="top"/>
|
||||
<xsd:enumeration value="bot"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
<xsd:complexType name="CT_TopBot">
|
||||
<xsd:attribute name="val" type="ST_TopBot" use="required"/>
|
||||
</xsd:complexType>
|
||||
<xsd:simpleType name="ST_Script">
|
||||
<xsd:restriction base="xsd:string">
|
||||
<xsd:enumeration value="roman"/>
|
||||
<xsd:enumeration value="script"/>
|
||||
<xsd:enumeration value="fraktur"/>
|
||||
<xsd:enumeration value="double-struck"/>
|
||||
<xsd:enumeration value="sans-serif"/>
|
||||
<xsd:enumeration value="monospace"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
<xsd:complexType name="CT_Script">
|
||||
<xsd:attribute name="val" type="ST_Script"/>
|
||||
</xsd:complexType>
|
||||
<xsd:simpleType name="ST_Style">
|
||||
<xsd:restriction base="xsd:string">
|
||||
<xsd:enumeration value="p"/>
|
||||
<xsd:enumeration value="b"/>
|
||||
<xsd:enumeration value="i"/>
|
||||
<xsd:enumeration value="bi"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
<xsd:complexType name="CT_Style">
|
||||
<xsd:attribute name="val" type="ST_Style"/>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_ManualBreak">
|
||||
<xsd:attribute name="alnAt" type="ST_Integer255"/>
|
||||
</xsd:complexType>
|
||||
<xsd:group name="EG_ScriptStyle">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="scr" minOccurs="0" type="CT_Script"/>
|
||||
<xsd:element name="sty" minOccurs="0" type="CT_Style"/>
|
||||
</xsd:sequence>
|
||||
</xsd:group>
|
||||
<xsd:complexType name="CT_RPR">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="lit" minOccurs="0" type="CT_OnOff"/>
|
||||
<xsd:choice>
|
||||
<xsd:element name="nor" minOccurs="0" type="CT_OnOff"/>
|
||||
<xsd:sequence>
|
||||
<xsd:group ref="EG_ScriptStyle"/>
|
||||
</xsd:sequence>
|
||||
</xsd:choice>
|
||||
<xsd:element name="brk" minOccurs="0" type="CT_ManualBreak"/>
|
||||
<xsd:element name="aln" minOccurs="0" type="CT_OnOff"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_Text">
|
||||
<xsd:simpleContent>
|
||||
<xsd:extension base="s:ST_String">
|
||||
<xsd:attribute ref="xml:space" use="optional"/>
|
||||
</xsd:extension>
|
||||
</xsd:simpleContent>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_R">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="rPr" type="CT_RPR" minOccurs="0"/>
|
||||
<xsd:group ref="w:EG_RPr" minOccurs="0"/>
|
||||
<xsd:choice minOccurs="0" maxOccurs="unbounded">
|
||||
<xsd:group ref="w:EG_RunInnerContent"/>
|
||||
<xsd:element name="t" type="CT_Text" minOccurs="0"/>
|
||||
</xsd:choice>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_CtrlPr">
|
||||
<xsd:sequence>
|
||||
<xsd:group ref="w:EG_RPrMath" minOccurs="0"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_AccPr">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="chr" type="CT_Char" minOccurs="0"/>
|
||||
<xsd:element name="ctrlPr" type="CT_CtrlPr" minOccurs="0"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_Acc">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="accPr" type="CT_AccPr" minOccurs="0"/>
|
||||
<xsd:element name="e" type="CT_OMathArg"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_BarPr">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="pos" type="CT_TopBot" minOccurs="0"/>
|
||||
<xsd:element name="ctrlPr" type="CT_CtrlPr" minOccurs="0"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_Bar">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="barPr" type="CT_BarPr" minOccurs="0"/>
|
||||
<xsd:element name="e" type="CT_OMathArg"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_BoxPr">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="opEmu" type="CT_OnOff" minOccurs="0"/>
|
||||
<xsd:element name="noBreak" type="CT_OnOff" minOccurs="0"/>
|
||||
<xsd:element name="diff" type="CT_OnOff" minOccurs="0"/>
|
||||
<xsd:element name="brk" type="CT_ManualBreak" minOccurs="0"/>
|
||||
<xsd:element name="aln" type="CT_OnOff" minOccurs="0"/>
|
||||
<xsd:element name="ctrlPr" type="CT_CtrlPr" minOccurs="0"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_Box">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="boxPr" type="CT_BoxPr" minOccurs="0"/>
|
||||
<xsd:element name="e" type="CT_OMathArg"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_BorderBoxPr">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="hideTop" type="CT_OnOff" minOccurs="0"/>
|
||||
<xsd:element name="hideBot" type="CT_OnOff" minOccurs="0"/>
|
||||
<xsd:element name="hideLeft" type="CT_OnOff" minOccurs="0"/>
|
||||
<xsd:element name="hideRight" type="CT_OnOff" minOccurs="0"/>
|
||||
<xsd:element name="strikeH" type="CT_OnOff" minOccurs="0"/>
|
||||
<xsd:element name="strikeV" type="CT_OnOff" minOccurs="0"/>
|
||||
<xsd:element name="strikeBLTR" type="CT_OnOff" minOccurs="0"/>
|
||||
<xsd:element name="strikeTLBR" type="CT_OnOff" minOccurs="0"/>
|
||||
<xsd:element name="ctrlPr" type="CT_CtrlPr" minOccurs="0"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_BorderBox">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="borderBoxPr" type="CT_BorderBoxPr" minOccurs="0"/>
|
||||
<xsd:element name="e" type="CT_OMathArg"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_DPr">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="begChr" type="CT_Char" minOccurs="0"/>
|
||||
<xsd:element name="sepChr" type="CT_Char" minOccurs="0"/>
|
||||
<xsd:element name="endChr" type="CT_Char" minOccurs="0"/>
|
||||
<xsd:element name="grow" type="CT_OnOff" minOccurs="0"/>
|
||||
<xsd:element name="shp" type="CT_Shp" minOccurs="0"/>
|
||||
<xsd:element name="ctrlPr" type="CT_CtrlPr" minOccurs="0"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_D">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="dPr" type="CT_DPr" minOccurs="0"/>
|
||||
<xsd:element name="e" type="CT_OMathArg" maxOccurs="unbounded"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_EqArrPr">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="baseJc" type="CT_YAlign" minOccurs="0"/>
|
||||
<xsd:element name="maxDist" type="CT_OnOff" minOccurs="0"/>
|
||||
<xsd:element name="objDist" type="CT_OnOff" minOccurs="0"/>
|
||||
<xsd:element name="rSpRule" type="CT_SpacingRule" minOccurs="0"/>
|
||||
<xsd:element name="rSp" type="CT_UnSignedInteger" minOccurs="0"/>
|
||||
<xsd:element name="ctrlPr" type="CT_CtrlPr" minOccurs="0"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_EqArr">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="eqArrPr" type="CT_EqArrPr" minOccurs="0"/>
|
||||
<xsd:element name="e" type="CT_OMathArg" maxOccurs="unbounded"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_FPr">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="type" type="CT_FType" minOccurs="0"/>
|
||||
<xsd:element name="ctrlPr" type="CT_CtrlPr" minOccurs="0"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_F">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="fPr" type="CT_FPr" minOccurs="0"/>
|
||||
<xsd:element name="num" type="CT_OMathArg"/>
|
||||
<xsd:element name="den" type="CT_OMathArg"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_FuncPr">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="ctrlPr" type="CT_CtrlPr" minOccurs="0"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_Func">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="funcPr" type="CT_FuncPr" minOccurs="0"/>
|
||||
<xsd:element name="fName" type="CT_OMathArg"/>
|
||||
<xsd:element name="e" type="CT_OMathArg"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_GroupChrPr">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="chr" type="CT_Char" minOccurs="0"/>
|
||||
<xsd:element name="pos" type="CT_TopBot" minOccurs="0"/>
|
||||
<xsd:element name="vertJc" type="CT_TopBot" minOccurs="0"/>
|
||||
<xsd:element name="ctrlPr" type="CT_CtrlPr" minOccurs="0"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_GroupChr">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="groupChrPr" type="CT_GroupChrPr" minOccurs="0"/>
|
||||
<xsd:element name="e" type="CT_OMathArg"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_LimLowPr">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="ctrlPr" type="CT_CtrlPr" minOccurs="0"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_LimLow">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="limLowPr" type="CT_LimLowPr" minOccurs="0"/>
|
||||
<xsd:element name="e" type="CT_OMathArg"/>
|
||||
<xsd:element name="lim" type="CT_OMathArg"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_LimUppPr">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="ctrlPr" type="CT_CtrlPr" minOccurs="0"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_LimUpp">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="limUppPr" type="CT_LimUppPr" minOccurs="0"/>
|
||||
<xsd:element name="e" type="CT_OMathArg"/>
|
||||
<xsd:element name="lim" type="CT_OMathArg"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_MCPr">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="count" type="CT_Integer255" minOccurs="0"/>
|
||||
<xsd:element name="mcJc" type="CT_XAlign" minOccurs="0"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_MC">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="mcPr" type="CT_MCPr" minOccurs="0"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_MCS">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="mc" type="CT_MC" maxOccurs="unbounded"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_MPr">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="baseJc" type="CT_YAlign" minOccurs="0"/>
|
||||
<xsd:element name="plcHide" type="CT_OnOff" minOccurs="0"/>
|
||||
<xsd:element name="rSpRule" type="CT_SpacingRule" minOccurs="0"/>
|
||||
<xsd:element name="cGpRule" type="CT_SpacingRule" minOccurs="0"/>
|
||||
<xsd:element name="rSp" type="CT_UnSignedInteger" minOccurs="0"/>
|
||||
<xsd:element name="cSp" type="CT_UnSignedInteger" minOccurs="0"/>
|
||||
<xsd:element name="cGp" type="CT_UnSignedInteger" minOccurs="0"/>
|
||||
<xsd:element name="mcs" type="CT_MCS" minOccurs="0"/>
|
||||
<xsd:element name="ctrlPr" type="CT_CtrlPr" minOccurs="0"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_MR">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="e" type="CT_OMathArg" maxOccurs="unbounded"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_M">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="mPr" type="CT_MPr" minOccurs="0"/>
|
||||
<xsd:element name="mr" type="CT_MR" maxOccurs="unbounded"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_NaryPr">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="chr" type="CT_Char" minOccurs="0"/>
|
||||
<xsd:element name="limLoc" type="CT_LimLoc" minOccurs="0"/>
|
||||
<xsd:element name="grow" type="CT_OnOff" minOccurs="0"/>
|
||||
<xsd:element name="subHide" type="CT_OnOff" minOccurs="0"/>
|
||||
<xsd:element name="supHide" type="CT_OnOff" minOccurs="0"/>
|
||||
<xsd:element name="ctrlPr" type="CT_CtrlPr" minOccurs="0"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_Nary">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="naryPr" type="CT_NaryPr" minOccurs="0"/>
|
||||
<xsd:element name="sub" type="CT_OMathArg"/>
|
||||
<xsd:element name="sup" type="CT_OMathArg"/>
|
||||
<xsd:element name="e" type="CT_OMathArg"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_PhantPr">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="show" type="CT_OnOff" minOccurs="0"/>
|
||||
<xsd:element name="zeroWid" type="CT_OnOff" minOccurs="0"/>
|
||||
<xsd:element name="zeroAsc" type="CT_OnOff" minOccurs="0"/>
|
||||
<xsd:element name="zeroDesc" type="CT_OnOff" minOccurs="0"/>
|
||||
<xsd:element name="transp" type="CT_OnOff" minOccurs="0"/>
|
||||
<xsd:element name="ctrlPr" type="CT_CtrlPr" minOccurs="0"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_Phant">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="phantPr" type="CT_PhantPr" minOccurs="0"/>
|
||||
<xsd:element name="e" type="CT_OMathArg"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_RadPr">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="degHide" type="CT_OnOff" minOccurs="0"/>
|
||||
<xsd:element name="ctrlPr" type="CT_CtrlPr" minOccurs="0"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_Rad">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="radPr" type="CT_RadPr" minOccurs="0"/>
|
||||
<xsd:element name="deg" type="CT_OMathArg"/>
|
||||
<xsd:element name="e" type="CT_OMathArg"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_SPrePr">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="ctrlPr" type="CT_CtrlPr" minOccurs="0"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_SPre">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="sPrePr" type="CT_SPrePr" minOccurs="0"/>
|
||||
<xsd:element name="sub" type="CT_OMathArg"/>
|
||||
<xsd:element name="sup" type="CT_OMathArg"/>
|
||||
<xsd:element name="e" type="CT_OMathArg"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_SSubPr">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="ctrlPr" type="CT_CtrlPr" minOccurs="0"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_SSub">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="sSubPr" type="CT_SSubPr" minOccurs="0"/>
|
||||
<xsd:element name="e" type="CT_OMathArg"/>
|
||||
<xsd:element name="sub" type="CT_OMathArg"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_SSubSupPr">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="alnScr" type="CT_OnOff" minOccurs="0"/>
|
||||
<xsd:element name="ctrlPr" type="CT_CtrlPr" minOccurs="0"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_SSubSup">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="sSubSupPr" type="CT_SSubSupPr" minOccurs="0"/>
|
||||
<xsd:element name="e" type="CT_OMathArg"/>
|
||||
<xsd:element name="sub" type="CT_OMathArg"/>
|
||||
<xsd:element name="sup" type="CT_OMathArg"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_SSupPr">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="ctrlPr" type="CT_CtrlPr" minOccurs="0"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_SSup">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="sSupPr" type="CT_SSupPr" minOccurs="0"/>
|
||||
<xsd:element name="e" type="CT_OMathArg"/>
|
||||
<xsd:element name="sup" type="CT_OMathArg"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:group name="EG_OMathMathElements">
|
||||
<xsd:choice>
|
||||
<xsd:element name="acc" type="CT_Acc"/>
|
||||
<xsd:element name="bar" type="CT_Bar"/>
|
||||
<xsd:element name="box" type="CT_Box"/>
|
||||
<xsd:element name="borderBox" type="CT_BorderBox"/>
|
||||
<xsd:element name="d" type="CT_D"/>
|
||||
<xsd:element name="eqArr" type="CT_EqArr"/>
|
||||
<xsd:element name="f" type="CT_F"/>
|
||||
<xsd:element name="func" type="CT_Func"/>
|
||||
<xsd:element name="groupChr" type="CT_GroupChr"/>
|
||||
<xsd:element name="limLow" type="CT_LimLow"/>
|
||||
<xsd:element name="limUpp" type="CT_LimUpp"/>
|
||||
<xsd:element name="m" type="CT_M"/>
|
||||
<xsd:element name="nary" type="CT_Nary"/>
|
||||
<xsd:element name="phant" type="CT_Phant"/>
|
||||
<xsd:element name="rad" type="CT_Rad"/>
|
||||
<xsd:element name="sPre" type="CT_SPre"/>
|
||||
<xsd:element name="sSub" type="CT_SSub"/>
|
||||
<xsd:element name="sSubSup" type="CT_SSubSup"/>
|
||||
<xsd:element name="sSup" type="CT_SSup"/>
|
||||
<xsd:element name="r" type="CT_R"/>
|
||||
</xsd:choice>
|
||||
</xsd:group>
|
||||
<xsd:group name="EG_OMathElements">
|
||||
<xsd:choice>
|
||||
<xsd:group ref="EG_OMathMathElements"/>
|
||||
<xsd:group ref="w:EG_PContentMath"/>
|
||||
</xsd:choice>
|
||||
</xsd:group>
|
||||
<xsd:complexType name="CT_OMathArgPr">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="argSz" type="CT_Integer2" minOccurs="0"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_OMathArg">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="argPr" type="CT_OMathArgPr" minOccurs="0"/>
|
||||
<xsd:group ref="EG_OMathElements" minOccurs="0" maxOccurs="unbounded"/>
|
||||
<xsd:element name="ctrlPr" type="CT_CtrlPr" minOccurs="0"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:simpleType name="ST_Jc">
|
||||
<xsd:restriction base="xsd:string">
|
||||
<xsd:enumeration value="left"/>
|
||||
<xsd:enumeration value="right"/>
|
||||
<xsd:enumeration value="center"/>
|
||||
<xsd:enumeration value="centerGroup"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
<xsd:complexType name="CT_OMathJc">
|
||||
<xsd:attribute name="val" type="ST_Jc"/>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_OMathParaPr">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="jc" type="CT_OMathJc" minOccurs="0"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_TwipsMeasure">
|
||||
<xsd:attribute name="val" type="s:ST_TwipsMeasure" use="required"/>
|
||||
</xsd:complexType>
|
||||
<xsd:simpleType name="ST_BreakBin">
|
||||
<xsd:restriction base="xsd:string">
|
||||
<xsd:enumeration value="before"/>
|
||||
<xsd:enumeration value="after"/>
|
||||
<xsd:enumeration value="repeat"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
<xsd:complexType name="CT_BreakBin">
|
||||
<xsd:attribute name="val" type="ST_BreakBin"/>
|
||||
</xsd:complexType>
|
||||
<xsd:simpleType name="ST_BreakBinSub">
|
||||
<xsd:restriction base="xsd:string">
|
||||
<xsd:enumeration value="--"/>
|
||||
<xsd:enumeration value="-+"/>
|
||||
<xsd:enumeration value="+-"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
<xsd:complexType name="CT_BreakBinSub">
|
||||
<xsd:attribute name="val" type="ST_BreakBinSub"/>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_MathPr">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="mathFont" type="CT_String" minOccurs="0"/>
|
||||
<xsd:element name="brkBin" type="CT_BreakBin" minOccurs="0"/>
|
||||
<xsd:element name="brkBinSub" type="CT_BreakBinSub" minOccurs="0"/>
|
||||
<xsd:element name="smallFrac" type="CT_OnOff" minOccurs="0"/>
|
||||
<xsd:element name="dispDef" type="CT_OnOff" minOccurs="0"/>
|
||||
<xsd:element name="lMargin" type="CT_TwipsMeasure" minOccurs="0"/>
|
||||
<xsd:element name="rMargin" type="CT_TwipsMeasure" minOccurs="0"/>
|
||||
<xsd:element name="defJc" type="CT_OMathJc" minOccurs="0"/>
|
||||
<xsd:element name="preSp" type="CT_TwipsMeasure" minOccurs="0"/>
|
||||
<xsd:element name="postSp" type="CT_TwipsMeasure" minOccurs="0"/>
|
||||
<xsd:element name="interSp" type="CT_TwipsMeasure" minOccurs="0"/>
|
||||
<xsd:element name="intraSp" type="CT_TwipsMeasure" minOccurs="0"/>
|
||||
<xsd:choice minOccurs="0">
|
||||
<xsd:element name="wrapIndent" type="CT_TwipsMeasure"/>
|
||||
<xsd:element name="wrapRight" type="CT_OnOff"/>
|
||||
</xsd:choice>
|
||||
<xsd:element name="intLim" type="CT_LimLoc" minOccurs="0"/>
|
||||
<xsd:element name="naryLim" type="CT_LimLoc" minOccurs="0"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:element name="mathPr" type="CT_MathPr"/>
|
||||
<xsd:complexType name="CT_OMathPara">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="oMathParaPr" type="CT_OMathParaPr" minOccurs="0"/>
|
||||
<xsd:element name="oMath" type="CT_OMath" maxOccurs="unbounded"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_OMath">
|
||||
<xsd:sequence>
|
||||
<xsd:group ref="EG_OMathElements" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:element name="oMathPara" type="CT_OMathPara"/>
|
||||
<xsd:element name="oMath" type="CT_OMath"/>
|
||||
</xsd:schema>
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
|
||||
xmlns="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
|
||||
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
|
||||
elementFormDefault="qualified"
|
||||
targetNamespace="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
|
||||
blockDefault="#all">
|
||||
<xsd:simpleType name="ST_RelationshipId">
|
||||
<xsd:restriction base="xsd:string"/>
|
||||
</xsd:simpleType>
|
||||
<xsd:attribute name="id" type="ST_RelationshipId"/>
|
||||
<xsd:attribute name="embed" type="ST_RelationshipId"/>
|
||||
<xsd:attribute name="link" type="ST_RelationshipId"/>
|
||||
<xsd:attribute name="dm" type="ST_RelationshipId" default=""/>
|
||||
<xsd:attribute name="lo" type="ST_RelationshipId" default=""/>
|
||||
<xsd:attribute name="qs" type="ST_RelationshipId" default=""/>
|
||||
<xsd:attribute name="cs" type="ST_RelationshipId" default=""/>
|
||||
<xsd:attribute name="blip" type="ST_RelationshipId" default=""/>
|
||||
<xsd:attribute name="pict" type="ST_RelationshipId"/>
|
||||
<xsd:attribute name="href" type="ST_RelationshipId"/>
|
||||
<xsd:attribute name="topLeft" type="ST_RelationshipId"/>
|
||||
<xsd:attribute name="topRight" type="ST_RelationshipId"/>
|
||||
<xsd:attribute name="bottomLeft" type="ST_RelationshipId"/>
|
||||
<xsd:attribute name="bottomRight" type="ST_RelationshipId"/>
|
||||
</xsd:schema>
|
||||
+4439
File diff suppressed because it is too large.
Load diff
+570
@@ -0,0 +1,570 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="urn:schemas-microsoft-com:vml"
|
||||
xmlns:pvml="urn:schemas-microsoft-com:office:powerpoint"
|
||||
xmlns:o="urn:schemas-microsoft-com:office:office"
|
||||
xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
|
||||
xmlns:w10="urn:schemas-microsoft-com:office:word"
|
||||
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
|
||||
xmlns:x="urn:schemas-microsoft-com:office:excel"
|
||||
xmlns:s="http://schemas.openxmlformats.org/officeDocument/2006/sharedTypes"
|
||||
targetNamespace="urn:schemas-microsoft-com:vml" elementFormDefault="qualified"
|
||||
attributeFormDefault="unqualified">
|
||||
<xsd:import namespace="urn:schemas-microsoft-com:office:office"
|
||||
schemaLocation="vml-officeDrawing.xsd"/>
|
||||
<xsd:import namespace="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
|
||||
schemaLocation="wml.xsd"/>
|
||||
<xsd:import namespace="urn:schemas-microsoft-com:office:word"
|
||||
schemaLocation="vml-wordprocessingDrawing.xsd"/>
|
||||
<xsd:import namespace="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
|
||||
schemaLocation="shared-relationshipReference.xsd"/>
|
||||
<xsd:import namespace="urn:schemas-microsoft-com:office:excel"
|
||||
schemaLocation="vml-spreadsheetDrawing.xsd"/>
|
||||
<xsd:import namespace="urn:schemas-microsoft-com:office:powerpoint"
|
||||
schemaLocation="vml-presentationDrawing.xsd"/>
|
||||
<xsd:import namespace="http://schemas.openxmlformats.org/officeDocument/2006/sharedTypes"
|
||||
schemaLocation="shared-commonSimpleTypes.xsd"/>
|
||||
<xsd:attributeGroup name="AG_Id">
|
||||
<xsd:attribute name="id" type="xsd:string" use="optional"/>
|
||||
</xsd:attributeGroup>
|
||||
<xsd:attributeGroup name="AG_Style">
|
||||
<xsd:attribute name="style" type="xsd:string" use="optional"/>
|
||||
</xsd:attributeGroup>
|
||||
<xsd:attributeGroup name="AG_Type">
|
||||
<xsd:attribute name="type" type="xsd:string" use="optional"/>
|
||||
</xsd:attributeGroup>
|
||||
<xsd:attributeGroup name="AG_Adj">
|
||||
<xsd:attribute name="adj" type="xsd:string" use="optional"/>
|
||||
</xsd:attributeGroup>
|
||||
<xsd:attributeGroup name="AG_Path">
|
||||
<xsd:attribute name="path" type="xsd:string" use="optional"/>
|
||||
</xsd:attributeGroup>
|
||||
<xsd:attributeGroup name="AG_Fill">
|
||||
<xsd:attribute name="filled" type="s:ST_TrueFalse" use="optional"/>
|
||||
<xsd:attribute name="fillcolor" type="s:ST_ColorType" use="optional"/>
|
||||
</xsd:attributeGroup>
|
||||
<xsd:attributeGroup name="AG_Chromakey">
|
||||
<xsd:attribute name="chromakey" type="s:ST_ColorType" use="optional"/>
|
||||
</xsd:attributeGroup>
|
||||
<xsd:attributeGroup name="AG_Ext">
|
||||
<xsd:attribute name="ext" form="qualified" type="ST_Ext"/>
|
||||
</xsd:attributeGroup>
|
||||
<xsd:attributeGroup name="AG_CoreAttributes">
|
||||
<xsd:attributeGroup ref="AG_Id"/>
|
||||
<xsd:attributeGroup ref="AG_Style"/>
|
||||
<xsd:attribute name="href" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="target" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="class" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="title" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="alt" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="coordsize" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="coordorigin" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="wrapcoords" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="print" type="s:ST_TrueFalse" use="optional"/>
|
||||
</xsd:attributeGroup>
|
||||
<xsd:attributeGroup name="AG_ShapeAttributes">
|
||||
<xsd:attributeGroup ref="AG_Chromakey"/>
|
||||
<xsd:attributeGroup ref="AG_Fill"/>
|
||||
<xsd:attribute name="opacity" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="stroked" type="s:ST_TrueFalse" use="optional"/>
|
||||
<xsd:attribute name="strokecolor" type="s:ST_ColorType" use="optional"/>
|
||||
<xsd:attribute name="strokeweight" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="insetpen" type="s:ST_TrueFalse" use="optional"/>
|
||||
</xsd:attributeGroup>
|
||||
<xsd:attributeGroup name="AG_OfficeCoreAttributes">
|
||||
<xsd:attribute ref="o:spid"/>
|
||||
<xsd:attribute ref="o:oned"/>
|
||||
<xsd:attribute ref="o:regroupid"/>
|
||||
<xsd:attribute ref="o:doubleclicknotify"/>
|
||||
<xsd:attribute ref="o:button"/>
|
||||
<xsd:attribute ref="o:userhidden"/>
|
||||
<xsd:attribute ref="o:bullet"/>
|
||||
<xsd:attribute ref="o:hr"/>
|
||||
<xsd:attribute ref="o:hrstd"/>
|
||||
<xsd:attribute ref="o:hrnoshade"/>
|
||||
<xsd:attribute ref="o:hrpct"/>
|
||||
<xsd:attribute ref="o:hralign"/>
|
||||
<xsd:attribute ref="o:allowincell"/>
|
||||
<xsd:attribute ref="o:allowoverlap"/>
|
||||
<xsd:attribute ref="o:userdrawn"/>
|
||||
<xsd:attribute ref="o:bordertopcolor"/>
|
||||
<xsd:attribute ref="o:borderleftcolor"/>
|
||||
<xsd:attribute ref="o:borderbottomcolor"/>
|
||||
<xsd:attribute ref="o:borderrightcolor"/>
|
||||
<xsd:attribute ref="o:dgmlayout"/>
|
||||
<xsd:attribute ref="o:dgmnodekind"/>
|
||||
<xsd:attribute ref="o:dgmlayoutmru"/>
|
||||
<xsd:attribute ref="o:insetmode"/>
|
||||
</xsd:attributeGroup>
|
||||
<xsd:attributeGroup name="AG_OfficeShapeAttributes">
|
||||
<xsd:attribute ref="o:spt"/>
|
||||
<xsd:attribute ref="o:connectortype"/>
|
||||
<xsd:attribute ref="o:bwmode"/>
|
||||
<xsd:attribute ref="o:bwpure"/>
|
||||
<xsd:attribute ref="o:bwnormal"/>
|
||||
<xsd:attribute ref="o:forcedash"/>
|
||||
<xsd:attribute ref="o:oleicon"/>
|
||||
<xsd:attribute ref="o:ole"/>
|
||||
<xsd:attribute ref="o:preferrelative"/>
|
||||
<xsd:attribute ref="o:cliptowrap"/>
|
||||
<xsd:attribute ref="o:clip"/>
|
||||
</xsd:attributeGroup>
|
||||
<xsd:attributeGroup name="AG_AllCoreAttributes">
|
||||
<xsd:attributeGroup ref="AG_CoreAttributes"/>
|
||||
<xsd:attributeGroup ref="AG_OfficeCoreAttributes"/>
|
||||
</xsd:attributeGroup>
|
||||
<xsd:attributeGroup name="AG_AllShapeAttributes">
|
||||
<xsd:attributeGroup ref="AG_ShapeAttributes"/>
|
||||
<xsd:attributeGroup ref="AG_OfficeShapeAttributes"/>
|
||||
</xsd:attributeGroup>
|
||||
<xsd:attributeGroup name="AG_ImageAttributes">
|
||||
<xsd:attribute name="src" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="cropleft" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="croptop" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="cropright" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="cropbottom" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="gain" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="blacklevel" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="gamma" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="grayscale" type="s:ST_TrueFalse" use="optional"/>
|
||||
<xsd:attribute name="bilevel" type="s:ST_TrueFalse" use="optional"/>
|
||||
</xsd:attributeGroup>
|
||||
<xsd:attributeGroup name="AG_StrokeAttributes">
|
||||
<xsd:attribute name="on" type="s:ST_TrueFalse" use="optional"/>
|
||||
<xsd:attribute name="weight" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="color" type="s:ST_ColorType" use="optional"/>
|
||||
<xsd:attribute name="opacity" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="linestyle" type="ST_StrokeLineStyle" use="optional"/>
|
||||
<xsd:attribute name="miterlimit" type="xsd:decimal" use="optional"/>
|
||||
<xsd:attribute name="joinstyle" type="ST_StrokeJoinStyle" use="optional"/>
|
||||
<xsd:attribute name="endcap" type="ST_StrokeEndCap" use="optional"/>
|
||||
<xsd:attribute name="dashstyle" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="filltype" type="ST_FillType" use="optional"/>
|
||||
<xsd:attribute name="src" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="imageaspect" type="ST_ImageAspect" use="optional"/>
|
||||
<xsd:attribute name="imagesize" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="imagealignshape" type="s:ST_TrueFalse" use="optional"/>
|
||||
<xsd:attribute name="color2" type="s:ST_ColorType" use="optional"/>
|
||||
<xsd:attribute name="startarrow" type="ST_StrokeArrowType" use="optional"/>
|
||||
<xsd:attribute name="startarrowwidth" type="ST_StrokeArrowWidth" use="optional"/>
|
||||
<xsd:attribute name="startarrowlength" type="ST_StrokeArrowLength" use="optional"/>
|
||||
<xsd:attribute name="endarrow" type="ST_StrokeArrowType" use="optional"/>
|
||||
<xsd:attribute name="endarrowwidth" type="ST_StrokeArrowWidth" use="optional"/>
|
||||
<xsd:attribute name="endarrowlength" type="ST_StrokeArrowLength" use="optional"/>
|
||||
<xsd:attribute ref="o:href"/>
|
||||
<xsd:attribute ref="o:althref"/>
|
||||
<xsd:attribute ref="o:title"/>
|
||||
<xsd:attribute ref="o:forcedash"/>
|
||||
<xsd:attribute ref="r:id" use="optional"/>
|
||||
<xsd:attribute name="insetpen" type="s:ST_TrueFalse" use="optional"/>
|
||||
<xsd:attribute ref="o:relid"/>
|
||||
</xsd:attributeGroup>
|
||||
<xsd:group name="EG_ShapeElements">
|
||||
<xsd:choice>
|
||||
<xsd:element ref="path"/>
|
||||
<xsd:element ref="formulas"/>
|
||||
<xsd:element ref="handles"/>
|
||||
<xsd:element ref="fill"/>
|
||||
<xsd:element ref="stroke"/>
|
||||
<xsd:element ref="shadow"/>
|
||||
<xsd:element ref="textbox"/>
|
||||
<xsd:element ref="textpath"/>
|
||||
<xsd:element ref="imagedata"/>
|
||||
<xsd:element ref="o:skew"/>
|
||||
<xsd:element ref="o:extrusion"/>
|
||||
<xsd:element ref="o:callout"/>
|
||||
<xsd:element ref="o:lock"/>
|
||||
<xsd:element ref="o:clippath"/>
|
||||
<xsd:element ref="o:signatureline"/>
|
||||
<xsd:element ref="w10:wrap"/>
|
||||
<xsd:element ref="w10:anchorlock"/>
|
||||
<xsd:element ref="w10:bordertop"/>
|
||||
<xsd:element ref="w10:borderbottom"/>
|
||||
<xsd:element ref="w10:borderleft"/>
|
||||
<xsd:element ref="w10:borderright"/>
|
||||
<xsd:element ref="x:ClientData" minOccurs="0"/>
|
||||
<xsd:element ref="pvml:textdata" minOccurs="0"/>
|
||||
</xsd:choice>
|
||||
</xsd:group>
|
||||
<xsd:element name="shape" type="CT_Shape"/>
|
||||
<xsd:element name="shapetype" type="CT_Shapetype"/>
|
||||
<xsd:element name="group" type="CT_Group"/>
|
||||
<xsd:element name="background" type="CT_Background"/>
|
||||
<xsd:complexType name="CT_Shape">
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:group ref="EG_ShapeElements"/>
|
||||
<xsd:element ref="o:ink"/>
|
||||
<xsd:element ref="pvml:iscomment"/>
|
||||
<xsd:element ref="o:equationxml"/>
|
||||
</xsd:choice>
|
||||
<xsd:attributeGroup ref="AG_AllCoreAttributes"/>
|
||||
<xsd:attributeGroup ref="AG_AllShapeAttributes"/>
|
||||
<xsd:attributeGroup ref="AG_Type"/>
|
||||
<xsd:attributeGroup ref="AG_Adj"/>
|
||||
<xsd:attributeGroup ref="AG_Path"/>
|
||||
<xsd:attribute ref="o:gfxdata"/>
|
||||
<xsd:attribute name="equationxml" type="xsd:string" use="optional"/>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_Shapetype">
|
||||
<xsd:sequence>
|
||||
<xsd:group ref="EG_ShapeElements" minOccurs="0" maxOccurs="unbounded"/>
|
||||
<xsd:element ref="o:complex" minOccurs="0"/>
|
||||
</xsd:sequence>
|
||||
<xsd:attributeGroup ref="AG_AllCoreAttributes"/>
|
||||
<xsd:attributeGroup ref="AG_AllShapeAttributes"/>
|
||||
<xsd:attributeGroup ref="AG_Adj"/>
|
||||
<xsd:attributeGroup ref="AG_Path"/>
|
||||
<xsd:attribute ref="o:master"/>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_Group">
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:group ref="EG_ShapeElements"/>
|
||||
<xsd:element ref="group"/>
|
||||
<xsd:element ref="shape"/>
|
||||
<xsd:element ref="shapetype"/>
|
||||
<xsd:element ref="arc"/>
|
||||
<xsd:element ref="curve"/>
|
||||
<xsd:element ref="image"/>
|
||||
<xsd:element ref="line"/>
|
||||
<xsd:element ref="oval"/>
|
||||
<xsd:element ref="polyline"/>
|
||||
<xsd:element ref="rect"/>
|
||||
<xsd:element ref="roundrect"/>
|
||||
<xsd:element ref="o:diagram"/>
|
||||
</xsd:choice>
|
||||
<xsd:attributeGroup ref="AG_AllCoreAttributes"/>
|
||||
<xsd:attributeGroup ref="AG_Fill"/>
|
||||
<xsd:attribute name="editas" type="ST_EditAs" use="optional"/>
|
||||
<xsd:attribute ref="o:tableproperties"/>
|
||||
<xsd:attribute ref="o:tablelimits"/>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_Background">
|
||||
<xsd:sequence>
|
||||
<xsd:element ref="fill" minOccurs="0"/>
|
||||
</xsd:sequence>
|
||||
<xsd:attributeGroup ref="AG_Id"/>
|
||||
<xsd:attributeGroup ref="AG_Fill"/>
|
||||
<xsd:attribute ref="o:bwmode"/>
|
||||
<xsd:attribute ref="o:bwpure"/>
|
||||
<xsd:attribute ref="o:bwnormal"/>
|
||||
<xsd:attribute ref="o:targetscreensize"/>
|
||||
</xsd:complexType>
|
||||
<xsd:element name="fill" type="CT_Fill"/>
|
||||
<xsd:element name="formulas" type="CT_Formulas"/>
|
||||
<xsd:element name="handles" type="CT_Handles"/>
|
||||
<xsd:element name="imagedata" type="CT_ImageData"/>
|
||||
<xsd:element name="path" type="CT_Path"/>
|
||||
<xsd:element name="textbox" type="CT_Textbox"/>
|
||||
<xsd:element name="shadow" type="CT_Shadow"/>
|
||||
<xsd:element name="stroke" type="CT_Stroke"/>
|
||||
<xsd:element name="textpath" type="CT_TextPath"/>
|
||||
<xsd:complexType name="CT_Fill">
|
||||
<xsd:sequence>
|
||||
<xsd:element ref="o:fill" minOccurs="0"/>
|
||||
</xsd:sequence>
|
||||
<xsd:attributeGroup ref="AG_Id"/>
|
||||
<xsd:attribute name="type" type="ST_FillType" use="optional"/>
|
||||
<xsd:attribute name="on" type="s:ST_TrueFalse" use="optional"/>
|
||||
<xsd:attribute name="color" type="s:ST_ColorType" use="optional"/>
|
||||
<xsd:attribute name="opacity" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="color2" type="s:ST_ColorType" use="optional"/>
|
||||
<xsd:attribute name="src" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute ref="o:href"/>
|
||||
<xsd:attribute ref="o:althref"/>
|
||||
<xsd:attribute name="size" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="origin" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="position" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="aspect" type="ST_ImageAspect" use="optional"/>
|
||||
<xsd:attribute name="colors" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="angle" type="xsd:decimal" use="optional"/>
|
||||
<xsd:attribute name="alignshape" type="s:ST_TrueFalse" use="optional"/>
|
||||
<xsd:attribute name="focus" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="focussize" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="focusposition" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="method" type="ST_FillMethod" use="optional"/>
|
||||
<xsd:attribute ref="o:detectmouseclick"/>
|
||||
<xsd:attribute ref="o:title"/>
|
||||
<xsd:attribute ref="o:opacity2"/>
|
||||
<xsd:attribute name="recolor" type="s:ST_TrueFalse" use="optional"/>
|
||||
<xsd:attribute name="rotate" type="s:ST_TrueFalse" use="optional"/>
|
||||
<xsd:attribute ref="r:id" use="optional"/>
|
||||
<xsd:attribute ref="o:relid" use="optional"/>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_Formulas">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="f" type="CT_F" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_F">
|
||||
<xsd:attribute name="eqn" type="xsd:string"/>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_Handles">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="h" type="CT_H" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_H">
|
||||
<xsd:attribute name="position" type="xsd:string"/>
|
||||
<xsd:attribute name="polar" type="xsd:string"/>
|
||||
<xsd:attribute name="map" type="xsd:string"/>
|
||||
<xsd:attribute name="invx" type="s:ST_TrueFalse"/>
|
||||
<xsd:attribute name="invy" type="s:ST_TrueFalse"/>
|
||||
<xsd:attribute name="switch" type="s:ST_TrueFalseBlank"/>
|
||||
<xsd:attribute name="xrange" type="xsd:string"/>
|
||||
<xsd:attribute name="yrange" type="xsd:string"/>
|
||||
<xsd:attribute name="radiusrange" type="xsd:string"/>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_ImageData">
|
||||
<xsd:attributeGroup ref="AG_Id"/>
|
||||
<xsd:attributeGroup ref="AG_ImageAttributes"/>
|
||||
<xsd:attributeGroup ref="AG_Chromakey"/>
|
||||
<xsd:attribute name="embosscolor" type="s:ST_ColorType" use="optional"/>
|
||||
<xsd:attribute name="recolortarget" type="s:ST_ColorType"/>
|
||||
<xsd:attribute ref="o:href"/>
|
||||
<xsd:attribute ref="o:althref"/>
|
||||
<xsd:attribute ref="o:title"/>
|
||||
<xsd:attribute ref="o:oleid"/>
|
||||
<xsd:attribute ref="o:detectmouseclick"/>
|
||||
<xsd:attribute ref="o:movie"/>
|
||||
<xsd:attribute ref="o:relid"/>
|
||||
<xsd:attribute ref="r:id"/>
|
||||
<xsd:attribute ref="r:pict"/>
|
||||
<xsd:attribute ref="r:href"/>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_Path">
|
||||
<xsd:attributeGroup ref="AG_Id"/>
|
||||
<xsd:attribute name="v" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="limo" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="textboxrect" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="fillok" type="s:ST_TrueFalse" use="optional"/>
|
||||
<xsd:attribute name="strokeok" type="s:ST_TrueFalse" use="optional"/>
|
||||
<xsd:attribute name="shadowok" type="s:ST_TrueFalse" use="optional"/>
|
||||
<xsd:attribute name="arrowok" type="s:ST_TrueFalse" use="optional"/>
|
||||
<xsd:attribute name="gradientshapeok" type="s:ST_TrueFalse" use="optional"/>
|
||||
<xsd:attribute name="textpathok" type="s:ST_TrueFalse" use="optional"/>
|
||||
<xsd:attribute name="insetpenok" type="s:ST_TrueFalse" use="optional"/>
|
||||
<xsd:attribute ref="o:connecttype"/>
|
||||
<xsd:attribute ref="o:connectlocs"/>
|
||||
<xsd:attribute ref="o:connectangles"/>
|
||||
<xsd:attribute ref="o:extrusionok"/>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_Shadow">
|
||||
<xsd:attributeGroup ref="AG_Id"/>
|
||||
<xsd:attribute name="on" type="s:ST_TrueFalse" use="optional"/>
|
||||
<xsd:attribute name="type" type="ST_ShadowType" use="optional"/>
|
||||
<xsd:attribute name="obscured" type="s:ST_TrueFalse" use="optional"/>
|
||||
<xsd:attribute name="color" type="s:ST_ColorType" use="optional"/>
|
||||
<xsd:attribute name="opacity" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="offset" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="color2" type="s:ST_ColorType" use="optional"/>
|
||||
<xsd:attribute name="offset2" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="origin" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="matrix" type="xsd:string" use="optional"/>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_Stroke">
|
||||
<xsd:sequence>
|
||||
<xsd:element ref="o:left" minOccurs="0"/>
|
||||
<xsd:element ref="o:top" minOccurs="0"/>
|
||||
<xsd:element ref="o:right" minOccurs="0"/>
|
||||
<xsd:element ref="o:bottom" minOccurs="0"/>
|
||||
<xsd:element ref="o:column" minOccurs="0"/>
|
||||
</xsd:sequence>
|
||||
<xsd:attributeGroup ref="AG_Id"/>
|
||||
<xsd:attributeGroup ref="AG_StrokeAttributes"/>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_Textbox">
|
||||
<xsd:choice>
|
||||
<xsd:element ref="w:txbxContent" minOccurs="0"/>
|
||||
<xsd:any namespace="##local" processContents="skip"/>
|
||||
</xsd:choice>
|
||||
<xsd:attributeGroup ref="AG_Id"/>
|
||||
<xsd:attributeGroup ref="AG_Style"/>
|
||||
<xsd:attribute name="inset" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute ref="o:singleclick"/>
|
||||
<xsd:attribute ref="o:insetmode"/>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_TextPath">
|
||||
<xsd:attributeGroup ref="AG_Id"/>
|
||||
<xsd:attributeGroup ref="AG_Style"/>
|
||||
<xsd:attribute name="on" type="s:ST_TrueFalse" use="optional"/>
|
||||
<xsd:attribute name="fitshape" type="s:ST_TrueFalse" use="optional"/>
|
||||
<xsd:attribute name="fitpath" type="s:ST_TrueFalse" use="optional"/>
|
||||
<xsd:attribute name="trim" type="s:ST_TrueFalse" use="optional"/>
|
||||
<xsd:attribute name="xscale" type="s:ST_TrueFalse" use="optional"/>
|
||||
<xsd:attribute name="string" type="xsd:string" use="optional"/>
|
||||
</xsd:complexType>
|
||||
<xsd:element name="arc" type="CT_Arc"/>
|
||||
<xsd:element name="curve" type="CT_Curve"/>
|
||||
<xsd:element name="image" type="CT_Image"/>
|
||||
<xsd:element name="line" type="CT_Line"/>
|
||||
<xsd:element name="oval" type="CT_Oval"/>
|
||||
<xsd:element name="polyline" type="CT_PolyLine"/>
|
||||
<xsd:element name="rect" type="CT_Rect"/>
|
||||
<xsd:element name="roundrect" type="CT_RoundRect"/>
|
||||
<xsd:complexType name="CT_Arc">
|
||||
<xsd:sequence>
|
||||
<xsd:group ref="EG_ShapeElements" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xsd:sequence>
|
||||
<xsd:attributeGroup ref="AG_AllCoreAttributes"/>
|
||||
<xsd:attributeGroup ref="AG_AllShapeAttributes"/>
|
||||
<xsd:attribute name="startAngle" type="xsd:decimal" use="optional"/>
|
||||
<xsd:attribute name="endAngle" type="xsd:decimal" use="optional"/>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_Curve">
|
||||
<xsd:sequence>
|
||||
<xsd:group ref="EG_ShapeElements" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xsd:sequence>
|
||||
<xsd:attributeGroup ref="AG_AllCoreAttributes"/>
|
||||
<xsd:attributeGroup ref="AG_AllShapeAttributes"/>
|
||||
<xsd:attribute name="from" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="control1" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="control2" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="to" type="xsd:string" use="optional"/>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_Image">
|
||||
<xsd:sequence>
|
||||
<xsd:group ref="EG_ShapeElements" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xsd:sequence>
|
||||
<xsd:attributeGroup ref="AG_AllCoreAttributes"/>
|
||||
<xsd:attributeGroup ref="AG_AllShapeAttributes"/>
|
||||
<xsd:attributeGroup ref="AG_ImageAttributes"/>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_Line">
|
||||
<xsd:sequence>
|
||||
<xsd:group ref="EG_ShapeElements" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xsd:sequence>
|
||||
<xsd:attributeGroup ref="AG_AllCoreAttributes"/>
|
||||
<xsd:attributeGroup ref="AG_AllShapeAttributes"/>
|
||||
<xsd:attribute name="from" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="to" type="xsd:string" use="optional"/>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_Oval">
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:group ref="EG_ShapeElements" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xsd:choice>
|
||||
<xsd:attributeGroup ref="AG_AllCoreAttributes"/>
|
||||
<xsd:attributeGroup ref="AG_AllShapeAttributes"/>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_PolyLine">
|
||||
<xsd:choice minOccurs="0" maxOccurs="unbounded">
|
||||
<xsd:group ref="EG_ShapeElements"/>
|
||||
<xsd:element ref="o:ink"/>
|
||||
</xsd:choice>
|
||||
<xsd:attributeGroup ref="AG_AllCoreAttributes"/>
|
||||
<xsd:attributeGroup ref="AG_AllShapeAttributes"/>
|
||||
<xsd:attribute name="points" type="xsd:string" use="optional"/>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_Rect">
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:group ref="EG_ShapeElements" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xsd:choice>
|
||||
<xsd:attributeGroup ref="AG_AllCoreAttributes"/>
|
||||
<xsd:attributeGroup ref="AG_AllShapeAttributes"/>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_RoundRect">
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:group ref="EG_ShapeElements" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xsd:choice>
|
||||
<xsd:attributeGroup ref="AG_AllCoreAttributes"/>
|
||||
<xsd:attributeGroup ref="AG_AllShapeAttributes"/>
|
||||
<xsd:attribute name="arcsize" type="xsd:string" use="optional"/>
|
||||
</xsd:complexType>
|
||||
<xsd:simpleType name="ST_Ext">
|
||||
<xsd:restriction base="xsd:string">
|
||||
<xsd:enumeration value="view"/>
|
||||
<xsd:enumeration value="edit"/>
|
||||
<xsd:enumeration value="backwardCompatible"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
<xsd:simpleType name="ST_FillType">
|
||||
<xsd:restriction base="xsd:string">
|
||||
<xsd:enumeration value="solid"/>
|
||||
<xsd:enumeration value="gradient"/>
|
||||
<xsd:enumeration value="gradientRadial"/>
|
||||
<xsd:enumeration value="tile"/>
|
||||
<xsd:enumeration value="pattern"/>
|
||||
<xsd:enumeration value="frame"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
<xsd:simpleType name="ST_FillMethod">
|
||||
<xsd:restriction base="xsd:string">
|
||||
<xsd:enumeration value="none"/>
|
||||
<xsd:enumeration value="linear"/>
|
||||
<xsd:enumeration value="sigma"/>
|
||||
<xsd:enumeration value="any"/>
|
||||
<xsd:enumeration value="linear sigma"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
<xsd:simpleType name="ST_ShadowType">
|
||||
<xsd:restriction base="xsd:string">
|
||||
<xsd:enumeration value="single"/>
|
||||
<xsd:enumeration value="double"/>
|
||||
<xsd:enumeration value="emboss"/>
|
||||
<xsd:enumeration value="perspective"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
<xsd:simpleType name="ST_StrokeLineStyle">
|
||||
<xsd:restriction base="xsd:string">
|
||||
<xsd:enumeration value="single"/>
|
||||
<xsd:enumeration value="thinThin"/>
|
||||
<xsd:enumeration value="thinThick"/>
|
||||
<xsd:enumeration value="thickThin"/>
|
||||
<xsd:enumeration value="thickBetweenThin"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
<xsd:simpleType name="ST_StrokeJoinStyle">
|
||||
<xsd:restriction base="xsd:string">
|
||||
<xsd:enumeration value="round"/>
|
||||
<xsd:enumeration value="bevel"/>
|
||||
<xsd:enumeration value="miter"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
<xsd:simpleType name="ST_StrokeEndCap">
|
||||
<xsd:restriction base="xsd:string">
|
||||
<xsd:enumeration value="flat"/>
|
||||
<xsd:enumeration value="square"/>
|
||||
<xsd:enumeration value="round"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
<xsd:simpleType name="ST_StrokeArrowLength">
|
||||
<xsd:restriction base="xsd:string">
|
||||
<xsd:enumeration value="short"/>
|
||||
<xsd:enumeration value="medium"/>
|
||||
<xsd:enumeration value="long"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
<xsd:simpleType name="ST_StrokeArrowWidth">
|
||||
<xsd:restriction base="xsd:string">
|
||||
<xsd:enumeration value="narrow"/>
|
||||
<xsd:enumeration value="medium"/>
|
||||
<xsd:enumeration value="wide"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
<xsd:simpleType name="ST_StrokeArrowType">
|
||||
<xsd:restriction base="xsd:string">
|
||||
<xsd:enumeration value="none"/>
|
||||
<xsd:enumeration value="block"/>
|
||||
<xsd:enumeration value="classic"/>
|
||||
<xsd:enumeration value="oval"/>
|
||||
<xsd:enumeration value="diamond"/>
|
||||
<xsd:enumeration value="open"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
<xsd:simpleType name="ST_ImageAspect">
|
||||
<xsd:restriction base="xsd:string">
|
||||
<xsd:enumeration value="ignore"/>
|
||||
<xsd:enumeration value="atMost"/>
|
||||
<xsd:enumeration value="atLeast"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
<xsd:simpleType name="ST_EditAs">
|
||||
<xsd:restriction base="xsd:string">
|
||||
<xsd:enumeration value="canvas"/>
|
||||
<xsd:enumeration value="orgchart"/>
|
||||
<xsd:enumeration value="radial"/>
|
||||
<xsd:enumeration value="cycle"/>
|
||||
<xsd:enumeration value="stacked"/>
|
||||
<xsd:enumeration value="venn"/>
|
||||
<xsd:enumeration value="bullseye"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
</xsd:schema>
|
||||
@@ -0,0 +1,509 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
|
||||
xmlns="urn:schemas-microsoft-com:office:office" xmlns:v="urn:schemas-microsoft-com:vml"
|
||||
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
|
||||
xmlns:s="http://schemas.openxmlformats.org/officeDocument/2006/sharedTypes"
|
||||
targetNamespace="urn:schemas-microsoft-com:office:office" elementFormDefault="qualified"
|
||||
attributeFormDefault="unqualified">
|
||||
<xsd:import namespace="urn:schemas-microsoft-com:vml" schemaLocation="vml-main.xsd"/>
|
||||
<xsd:import namespace="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
|
||||
schemaLocation="shared-relationshipReference.xsd"/>
|
||||
<xsd:import namespace="http://schemas.openxmlformats.org/officeDocument/2006/sharedTypes"
|
||||
schemaLocation="shared-commonSimpleTypes.xsd"/>
|
||||
<xsd:attribute name="bwmode" type="ST_BWMode"/>
|
||||
<xsd:attribute name="bwpure" type="ST_BWMode"/>
|
||||
<xsd:attribute name="bwnormal" type="ST_BWMode"/>
|
||||
<xsd:attribute name="targetscreensize" type="ST_ScreenSize"/>
|
||||
<xsd:attribute name="insetmode" type="ST_InsetMode" default="custom"/>
|
||||
<xsd:attribute name="spt" type="xsd:float"/>
|
||||
<xsd:attribute name="wrapcoords" type="xsd:string"/>
|
||||
<xsd:attribute name="oned" type="s:ST_TrueFalse"/>
|
||||
<xsd:attribute name="regroupid" type="xsd:integer"/>
|
||||
<xsd:attribute name="doubleclicknotify" type="s:ST_TrueFalse"/>
|
||||
<xsd:attribute name="connectortype" type="ST_ConnectorType" default="straight"/>
|
||||
<xsd:attribute name="button" type="s:ST_TrueFalse"/>
|
||||
<xsd:attribute name="userhidden" type="s:ST_TrueFalse"/>
|
||||
<xsd:attribute name="forcedash" type="s:ST_TrueFalse"/>
|
||||
<xsd:attribute name="oleicon" type="s:ST_TrueFalse"/>
|
||||
<xsd:attribute name="ole" type="s:ST_TrueFalseBlank"/>
|
||||
<xsd:attribute name="preferrelative" type="s:ST_TrueFalse"/>
|
||||
<xsd:attribute name="cliptowrap" type="s:ST_TrueFalse"/>
|
||||
<xsd:attribute name="clip" type="s:ST_TrueFalse"/>
|
||||
<xsd:attribute name="bullet" type="s:ST_TrueFalse"/>
|
||||
<xsd:attribute name="hr" type="s:ST_TrueFalse"/>
|
||||
<xsd:attribute name="hrstd" type="s:ST_TrueFalse"/>
|
||||
<xsd:attribute name="hrnoshade" type="s:ST_TrueFalse"/>
|
||||
<xsd:attribute name="hrpct" type="xsd:float"/>
|
||||
<xsd:attribute name="hralign" type="ST_HrAlign" default="left"/>
|
||||
<xsd:attribute name="allowincell" type="s:ST_TrueFalse"/>
|
||||
<xsd:attribute name="allowoverlap" type="s:ST_TrueFalse"/>
|
||||
<xsd:attribute name="userdrawn" type="s:ST_TrueFalse"/>
|
||||
<xsd:attribute name="bordertopcolor" type="xsd:string"/>
|
||||
<xsd:attribute name="borderleftcolor" type="xsd:string"/>
|
||||
<xsd:attribute name="borderbottomcolor" type="xsd:string"/>
|
||||
<xsd:attribute name="borderrightcolor" type="xsd:string"/>
|
||||
<xsd:attribute name="connecttype" type="ST_ConnectType"/>
|
||||
<xsd:attribute name="connectlocs" type="xsd:string"/>
|
||||
<xsd:attribute name="connectangles" type="xsd:string"/>
|
||||
<xsd:attribute name="master" type="xsd:string"/>
|
||||
<xsd:attribute name="extrusionok" type="s:ST_TrueFalse"/>
|
||||
<xsd:attribute name="href" type="xsd:string"/>
|
||||
<xsd:attribute name="althref" type="xsd:string"/>
|
||||
<xsd:attribute name="title" type="xsd:string"/>
|
||||
<xsd:attribute name="singleclick" type="s:ST_TrueFalse"/>
|
||||
<xsd:attribute name="oleid" type="xsd:float"/>
|
||||
<xsd:attribute name="detectmouseclick" type="s:ST_TrueFalse"/>
|
||||
<xsd:attribute name="movie" type="xsd:float"/>
|
||||
<xsd:attribute name="spid" type="xsd:string"/>
|
||||
<xsd:attribute name="opacity2" type="xsd:string"/>
|
||||
<xsd:attribute name="relid" type="r:ST_RelationshipId"/>
|
||||
<xsd:attribute name="dgmlayout" type="ST_DiagramLayout"/>
|
||||
<xsd:attribute name="dgmnodekind" type="xsd:integer"/>
|
||||
<xsd:attribute name="dgmlayoutmru" type="ST_DiagramLayout"/>
|
||||
<xsd:attribute name="gfxdata" type="xsd:base64Binary"/>
|
||||
<xsd:attribute name="tableproperties" type="xsd:string"/>
|
||||
<xsd:attribute name="tablelimits" type="xsd:string"/>
|
||||
<xsd:element name="shapedefaults" type="CT_ShapeDefaults"/>
|
||||
<xsd:element name="shapelayout" type="CT_ShapeLayout"/>
|
||||
<xsd:element name="signatureline" type="CT_SignatureLine"/>
|
||||
<xsd:element name="ink" type="CT_Ink"/>
|
||||
<xsd:element name="diagram" type="CT_Diagram"/>
|
||||
<xsd:element name="equationxml" type="CT_EquationXml"/>
|
||||
<xsd:complexType name="CT_ShapeDefaults">
|
||||
<xsd:all minOccurs="0">
|
||||
<xsd:element ref="v:fill" minOccurs="0"/>
|
||||
<xsd:element ref="v:stroke" minOccurs="0"/>
|
||||
<xsd:element ref="v:textbox" minOccurs="0"/>
|
||||
<xsd:element ref="v:shadow" minOccurs="0"/>
|
||||
<xsd:element ref="skew" minOccurs="0"/>
|
||||
<xsd:element ref="extrusion" minOccurs="0"/>
|
||||
<xsd:element ref="callout" minOccurs="0"/>
|
||||
<xsd:element ref="lock" minOccurs="0"/>
|
||||
<xsd:element name="colormru" minOccurs="0" type="CT_ColorMru"/>
|
||||
<xsd:element name="colormenu" minOccurs="0" type="CT_ColorMenu"/>
|
||||
</xsd:all>
|
||||
<xsd:attributeGroup ref="v:AG_Ext"/>
|
||||
<xsd:attribute name="spidmax" type="xsd:integer" use="optional"/>
|
||||
<xsd:attribute name="style" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="fill" type="s:ST_TrueFalse" use="optional"/>
|
||||
<xsd:attribute name="fillcolor" type="s:ST_ColorType" use="optional"/>
|
||||
<xsd:attribute name="stroke" type="s:ST_TrueFalse" use="optional"/>
|
||||
<xsd:attribute name="strokecolor" type="s:ST_ColorType"/>
|
||||
<xsd:attribute name="allowincell" form="qualified" type="s:ST_TrueFalse"/>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_Ink">
|
||||
<xsd:sequence/>
|
||||
<xsd:attribute name="i" type="xsd:string"/>
|
||||
<xsd:attribute name="annotation" type="s:ST_TrueFalse"/>
|
||||
<xsd:attribute name="contentType" type="ST_ContentType" use="optional"/>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_SignatureLine">
|
||||
<xsd:attributeGroup ref="v:AG_Ext"/>
|
||||
<xsd:attribute name="issignatureline" type="s:ST_TrueFalse"/>
|
||||
<xsd:attribute name="id" type="s:ST_Guid"/>
|
||||
<xsd:attribute name="provid" type="s:ST_Guid"/>
|
||||
<xsd:attribute name="signinginstructionsset" type="s:ST_TrueFalse"/>
|
||||
<xsd:attribute name="allowcomments" type="s:ST_TrueFalse"/>
|
||||
<xsd:attribute name="showsigndate" type="s:ST_TrueFalse"/>
|
||||
<xsd:attribute name="suggestedsigner" type="xsd:string" form="qualified"/>
|
||||
<xsd:attribute name="suggestedsigner2" type="xsd:string" form="qualified"/>
|
||||
<xsd:attribute name="suggestedsigneremail" type="xsd:string" form="qualified"/>
|
||||
<xsd:attribute name="signinginstructions" type="xsd:string"/>
|
||||
<xsd:attribute name="addlxml" type="xsd:string"/>
|
||||
<xsd:attribute name="sigprovurl" type="xsd:string"/>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_ShapeLayout">
|
||||
<xsd:all>
|
||||
<xsd:element name="idmap" type="CT_IdMap" minOccurs="0"/>
|
||||
<xsd:element name="regrouptable" type="CT_RegroupTable" minOccurs="0"/>
|
||||
<xsd:element name="rules" type="CT_Rules" minOccurs="0"/>
|
||||
</xsd:all>
|
||||
<xsd:attributeGroup ref="v:AG_Ext"/>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_IdMap">
|
||||
<xsd:attributeGroup ref="v:AG_Ext"/>
|
||||
<xsd:attribute name="data" type="xsd:string" use="optional"/>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_RegroupTable">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="entry" type="CT_Entry" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xsd:sequence>
|
||||
<xsd:attributeGroup ref="v:AG_Ext"/>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_Entry">
|
||||
<xsd:attribute name="new" type="xsd:int" use="optional"/>
|
||||
<xsd:attribute name="old" type="xsd:int" use="optional"/>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_Rules">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="r" type="CT_R" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xsd:sequence>
|
||||
<xsd:attributeGroup ref="v:AG_Ext"/>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_R">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="proxy" type="CT_Proxy" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="id" type="xsd:string" use="required"/>
|
||||
<xsd:attribute name="type" type="ST_RType" use="optional"/>
|
||||
<xsd:attribute name="how" type="ST_How" use="optional"/>
|
||||
<xsd:attribute name="idref" type="xsd:string" use="optional"/>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_Proxy">
|
||||
<xsd:attribute name="start" type="s:ST_TrueFalseBlank" use="optional" default="false"/>
|
||||
<xsd:attribute name="end" type="s:ST_TrueFalseBlank" use="optional" default="false"/>
|
||||
<xsd:attribute name="idref" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="connectloc" type="xsd:int" use="optional"/>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_Diagram">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="relationtable" type="CT_RelationTable" minOccurs="0"/>
|
||||
</xsd:sequence>
|
||||
<xsd:attributeGroup ref="v:AG_Ext"/>
|
||||
<xsd:attribute name="dgmstyle" type="xsd:integer" use="optional"/>
|
||||
<xsd:attribute name="autoformat" type="s:ST_TrueFalse" use="optional"/>
|
||||
<xsd:attribute name="reverse" type="s:ST_TrueFalse" use="optional"/>
|
||||
<xsd:attribute name="autolayout" type="s:ST_TrueFalse" use="optional"/>
|
||||
<xsd:attribute name="dgmscalex" type="xsd:integer" use="optional"/>
|
||||
<xsd:attribute name="dgmscaley" type="xsd:integer" use="optional"/>
|
||||
<xsd:attribute name="dgmfontsize" type="xsd:integer" use="optional"/>
|
||||
<xsd:attribute name="constrainbounds" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="dgmbasetextscale" type="xsd:integer" use="optional"/>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_EquationXml">
|
||||
<xsd:sequence>
|
||||
<xsd:any namespace="##any"/>
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="contentType" type="ST_AlternateMathContentType" use="optional"/>
|
||||
</xsd:complexType>
|
||||
<xsd:simpleType name="ST_AlternateMathContentType">
|
||||
<xsd:restriction base="xsd:string"/>
|
||||
</xsd:simpleType>
|
||||
<xsd:complexType name="CT_RelationTable">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="rel" type="CT_Relation" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xsd:sequence>
|
||||
<xsd:attributeGroup ref="v:AG_Ext"/>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_Relation">
|
||||
<xsd:attributeGroup ref="v:AG_Ext"/>
|
||||
<xsd:attribute name="idsrc" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="iddest" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="idcntr" type="xsd:string" use="optional"/>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_ColorMru">
|
||||
<xsd:attributeGroup ref="v:AG_Ext"/>
|
||||
<xsd:attribute name="colors" type="xsd:string"/>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_ColorMenu">
|
||||
<xsd:attributeGroup ref="v:AG_Ext"/>
|
||||
<xsd:attribute name="strokecolor" type="s:ST_ColorType"/>
|
||||
<xsd:attribute name="fillcolor" type="s:ST_ColorType"/>
|
||||
<xsd:attribute name="shadowcolor" type="s:ST_ColorType"/>
|
||||
<xsd:attribute name="extrusioncolor" type="s:ST_ColorType"/>
|
||||
</xsd:complexType>
|
||||
<xsd:element name="skew" type="CT_Skew"/>
|
||||
<xsd:element name="extrusion" type="CT_Extrusion"/>
|
||||
<xsd:element name="callout" type="CT_Callout"/>
|
||||
<xsd:element name="lock" type="CT_Lock"/>
|
||||
<xsd:element name="OLEObject" type="CT_OLEObject"/>
|
||||
<xsd:element name="complex" type="CT_Complex"/>
|
||||
<xsd:element name="left" type="CT_StrokeChild"/>
|
||||
<xsd:element name="top" type="CT_StrokeChild"/>
|
||||
<xsd:element name="right" type="CT_StrokeChild"/>
|
||||
<xsd:element name="bottom" type="CT_StrokeChild"/>
|
||||
<xsd:element name="column" type="CT_StrokeChild"/>
|
||||
<xsd:element name="clippath" type="CT_ClipPath"/>
|
||||
<xsd:element name="fill" type="CT_Fill"/>
|
||||
<xsd:complexType name="CT_Skew">
|
||||
<xsd:attributeGroup ref="v:AG_Ext"/>
|
||||
<xsd:attribute name="id" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="on" type="s:ST_TrueFalse" use="optional"/>
|
||||
<xsd:attribute name="offset" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="origin" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="matrix" type="xsd:string" use="optional"/>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_Extrusion">
|
||||
<xsd:attributeGroup ref="v:AG_Ext"/>
|
||||
<xsd:attribute name="on" type="s:ST_TrueFalse" use="optional"/>
|
||||
<xsd:attribute name="type" type="ST_ExtrusionType" default="parallel" use="optional"/>
|
||||
<xsd:attribute name="render" type="ST_ExtrusionRender" default="solid" use="optional"/>
|
||||
<xsd:attribute name="viewpointorigin" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="viewpoint" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="plane" type="ST_ExtrusionPlane" default="XY" use="optional"/>
|
||||
<xsd:attribute name="skewangle" type="xsd:float" use="optional"/>
|
||||
<xsd:attribute name="skewamt" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="foredepth" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="backdepth" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="orientation" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="orientationangle" type="xsd:float" use="optional"/>
|
||||
<xsd:attribute name="lockrotationcenter" type="s:ST_TrueFalse" use="optional"/>
|
||||
<xsd:attribute name="autorotationcenter" type="s:ST_TrueFalse" use="optional"/>
|
||||
<xsd:attribute name="rotationcenter" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="rotationangle" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="colormode" type="ST_ColorMode" use="optional"/>
|
||||
<xsd:attribute name="color" type="s:ST_ColorType" use="optional"/>
|
||||
<xsd:attribute name="shininess" type="xsd:float" use="optional"/>
|
||||
<xsd:attribute name="specularity" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="diffusity" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="metal" type="s:ST_TrueFalse" use="optional"/>
|
||||
<xsd:attribute name="edge" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="facet" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="lightface" type="s:ST_TrueFalse" use="optional"/>
|
||||
<xsd:attribute name="brightness" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="lightposition" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="lightlevel" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="lightharsh" type="s:ST_TrueFalse" use="optional"/>
|
||||
<xsd:attribute name="lightposition2" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="lightlevel2" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="lightharsh2" type="s:ST_TrueFalse" use="optional"/>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_Callout">
|
||||
<xsd:attributeGroup ref="v:AG_Ext"/>
|
||||
<xsd:attribute name="on" type="s:ST_TrueFalse" use="optional"/>
|
||||
<xsd:attribute name="type" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="gap" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="angle" type="ST_Angle" use="optional"/>
|
||||
<xsd:attribute name="dropauto" type="s:ST_TrueFalse" use="optional"/>
|
||||
<xsd:attribute name="drop" type="ST_CalloutDrop" use="optional"/>
|
||||
<xsd:attribute name="distance" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="lengthspecified" type="s:ST_TrueFalse" default="f" use="optional"/>
|
||||
<xsd:attribute name="length" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="accentbar" type="s:ST_TrueFalse" use="optional"/>
|
||||
<xsd:attribute name="textborder" type="s:ST_TrueFalse" use="optional"/>
|
||||
<xsd:attribute name="minusx" type="s:ST_TrueFalse" use="optional"/>
|
||||
<xsd:attribute name="minusy" type="s:ST_TrueFalse" use="optional"/>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_Lock">
|
||||
<xsd:attributeGroup ref="v:AG_Ext"/>
|
||||
<xsd:attribute name="position" type="s:ST_TrueFalse" use="optional"/>
|
||||
<xsd:attribute name="selection" type="s:ST_TrueFalse" use="optional"/>
|
||||
<xsd:attribute name="grouping" type="s:ST_TrueFalse" use="optional"/>
|
||||
<xsd:attribute name="ungrouping" type="s:ST_TrueFalse" use="optional"/>
|
||||
<xsd:attribute name="rotation" type="s:ST_TrueFalse" use="optional"/>
|
||||
<xsd:attribute name="cropping" type="s:ST_TrueFalse" use="optional"/>
|
||||
<xsd:attribute name="verticies" type="s:ST_TrueFalse" use="optional"/>
|
||||
<xsd:attribute name="adjusthandles" type="s:ST_TrueFalse" use="optional"/>
|
||||
<xsd:attribute name="text" type="s:ST_TrueFalse" use="optional"/>
|
||||
<xsd:attribute name="aspectratio" type="s:ST_TrueFalse" use="optional"/>
|
||||
<xsd:attribute name="shapetype" type="s:ST_TrueFalse" use="optional"/>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_OLEObject">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="LinkType" type="ST_OLELinkType" minOccurs="0"/>
|
||||
<xsd:element name="LockedField" type="s:ST_TrueFalseBlank" minOccurs="0"/>
|
||||
<xsd:element name="FieldCodes" type="xsd:string" minOccurs="0"/>
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="Type" type="ST_OLEType" use="optional"/>
|
||||
<xsd:attribute name="ProgID" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="ShapeID" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="DrawAspect" type="ST_OLEDrawAspect" use="optional"/>
|
||||
<xsd:attribute name="ObjectID" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute ref="r:id" use="optional"/>
|
||||
<xsd:attribute name="UpdateMode" type="ST_OLEUpdateMode" use="optional"/>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_Complex">
|
||||
<xsd:attributeGroup ref="v:AG_Ext"/>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_StrokeChild">
|
||||
<xsd:attributeGroup ref="v:AG_Ext"/>
|
||||
<xsd:attribute name="on" type="s:ST_TrueFalse" use="optional"/>
|
||||
<xsd:attribute name="weight" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="color" type="s:ST_ColorType" use="optional"/>
|
||||
<xsd:attribute name="color2" type="s:ST_ColorType" use="optional"/>
|
||||
<xsd:attribute name="opacity" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="linestyle" type="v:ST_StrokeLineStyle" use="optional"/>
|
||||
<xsd:attribute name="miterlimit" type="xsd:decimal" use="optional"/>
|
||||
<xsd:attribute name="joinstyle" type="v:ST_StrokeJoinStyle" use="optional"/>
|
||||
<xsd:attribute name="endcap" type="v:ST_StrokeEndCap" use="optional"/>
|
||||
<xsd:attribute name="dashstyle" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="insetpen" type="s:ST_TrueFalse" use="optional"/>
|
||||
<xsd:attribute name="filltype" type="v:ST_FillType" use="optional"/>
|
||||
<xsd:attribute name="src" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="imageaspect" type="v:ST_ImageAspect" use="optional"/>
|
||||
<xsd:attribute name="imagesize" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="imagealignshape" type="s:ST_TrueFalse" use="optional"/>
|
||||
<xsd:attribute name="startarrow" type="v:ST_StrokeArrowType" use="optional"/>
|
||||
<xsd:attribute name="startarrowwidth" type="v:ST_StrokeArrowWidth" use="optional"/>
|
||||
<xsd:attribute name="startarrowlength" type="v:ST_StrokeArrowLength" use="optional"/>
|
||||
<xsd:attribute name="endarrow" type="v:ST_StrokeArrowType" use="optional"/>
|
||||
<xsd:attribute name="endarrowwidth" type="v:ST_StrokeArrowWidth" use="optional"/>
|
||||
<xsd:attribute name="endarrowlength" type="v:ST_StrokeArrowLength" use="optional"/>
|
||||
<xsd:attribute ref="href"/>
|
||||
<xsd:attribute ref="althref"/>
|
||||
<xsd:attribute ref="title"/>
|
||||
<xsd:attribute ref="forcedash"/>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_ClipPath">
|
||||
<xsd:attribute name="v" type="xsd:string" use="required" form="qualified"/>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_Fill">
|
||||
<xsd:attributeGroup ref="v:AG_Ext"/>
|
||||
<xsd:attribute name="type" type="ST_FillType"/>
|
||||
</xsd:complexType>
|
||||
<xsd:simpleType name="ST_RType">
|
||||
<xsd:restriction base="xsd:string">
|
||||
<xsd:enumeration value="arc"/>
|
||||
<xsd:enumeration value="callout"/>
|
||||
<xsd:enumeration value="connector"/>
|
||||
<xsd:enumeration value="align"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
<xsd:simpleType name="ST_How">
|
||||
<xsd:restriction base="xsd:string">
|
||||
<xsd:enumeration value="top"/>
|
||||
<xsd:enumeration value="middle"/>
|
||||
<xsd:enumeration value="bottom"/>
|
||||
<xsd:enumeration value="left"/>
|
||||
<xsd:enumeration value="center"/>
|
||||
<xsd:enumeration value="right"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
<xsd:simpleType name="ST_BWMode">
|
||||
<xsd:restriction base="xsd:string">
|
||||
<xsd:enumeration value="color"/>
|
||||
<xsd:enumeration value="auto"/>
|
||||
<xsd:enumeration value="grayScale"/>
|
||||
<xsd:enumeration value="lightGrayscale"/>
|
||||
<xsd:enumeration value="inverseGray"/>
|
||||
<xsd:enumeration value="grayOutline"/>
|
||||
<xsd:enumeration value="highContrast"/>
|
||||
<xsd:enumeration value="black"/>
|
||||
<xsd:enumeration value="white"/>
|
||||
<xsd:enumeration value="hide"/>
|
||||
<xsd:enumeration value="undrawn"/>
|
||||
<xsd:enumeration value="blackTextAndLines"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
<xsd:simpleType name="ST_ScreenSize">
|
||||
<xsd:restriction base="xsd:string">
|
||||
<xsd:enumeration value="544,376"/>
|
||||
<xsd:enumeration value="640,480"/>
|
||||
<xsd:enumeration value="720,512"/>
|
||||
<xsd:enumeration value="800,600"/>
|
||||
<xsd:enumeration value="1024,768"/>
|
||||
<xsd:enumeration value="1152,862"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
<xsd:simpleType name="ST_InsetMode">
|
||||
<xsd:restriction base="xsd:string">
|
||||
<xsd:enumeration value="auto"/>
|
||||
<xsd:enumeration value="custom"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
<xsd:simpleType name="ST_ColorMode">
|
||||
<xsd:restriction base="xsd:string">
|
||||
<xsd:enumeration value="auto"/>
|
||||
<xsd:enumeration value="custom"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
<xsd:simpleType name="ST_ContentType">
|
||||
<xsd:restriction base="xsd:string"/>
|
||||
</xsd:simpleType>
|
||||
<xsd:simpleType name="ST_DiagramLayout">
|
||||
<xsd:restriction base="xsd:integer">
|
||||
<xsd:enumeration value="0"/>
|
||||
<xsd:enumeration value="1"/>
|
||||
<xsd:enumeration value="2"/>
|
||||
<xsd:enumeration value="3"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
<xsd:simpleType name="ST_ExtrusionType">
|
||||
<xsd:restriction base="xsd:string">
|
||||
<xsd:enumeration value="perspective"/>
|
||||
<xsd:enumeration value="parallel"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
<xsd:simpleType name="ST_ExtrusionRender">
|
||||
<xsd:restriction base="xsd:string">
|
||||
<xsd:enumeration value="solid"/>
|
||||
<xsd:enumeration value="wireFrame"/>
|
||||
<xsd:enumeration value="boundingCube"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
<xsd:simpleType name="ST_ExtrusionPlane">
|
||||
<xsd:restriction base="xsd:string">
|
||||
<xsd:enumeration value="XY"/>
|
||||
<xsd:enumeration value="ZX"/>
|
||||
<xsd:enumeration value="YZ"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
<xsd:simpleType name="ST_Angle">
|
||||
<xsd:restriction base="xsd:string">
|
||||
<xsd:enumeration value="any"/>
|
||||
<xsd:enumeration value="30"/>
|
||||
<xsd:enumeration value="45"/>
|
||||
<xsd:enumeration value="60"/>
|
||||
<xsd:enumeration value="90"/>
|
||||
<xsd:enumeration value="auto"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
<xsd:simpleType name="ST_CalloutDrop">
|
||||
<xsd:restriction base="xsd:string"/>
|
||||
</xsd:simpleType>
|
||||
<xsd:simpleType name="ST_CalloutPlacement">
|
||||
<xsd:restriction base="xsd:string">
|
||||
<xsd:enumeration value="top"/>
|
||||
<xsd:enumeration value="center"/>
|
||||
<xsd:enumeration value="bottom"/>
|
||||
<xsd:enumeration value="user"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
<xsd:simpleType name="ST_ConnectorType">
|
||||
<xsd:restriction base="xsd:string">
|
||||
<xsd:enumeration value="none"/>
|
||||
<xsd:enumeration value="straight"/>
|
||||
<xsd:enumeration value="elbow"/>
|
||||
<xsd:enumeration value="curved"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
<xsd:simpleType name="ST_HrAlign">
|
||||
<xsd:restriction base="xsd:string">
|
||||
<xsd:enumeration value="left"/>
|
||||
<xsd:enumeration value="right"/>
|
||||
<xsd:enumeration value="center"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
<xsd:simpleType name="ST_ConnectType">
|
||||
<xsd:restriction base="xsd:string">
|
||||
<xsd:enumeration value="none"/>
|
||||
<xsd:enumeration value="rect"/>
|
||||
<xsd:enumeration value="segments"/>
|
||||
<xsd:enumeration value="custom"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
<xsd:simpleType name="ST_OLELinkType">
|
||||
<xsd:restriction base="xsd:string"/>
|
||||
</xsd:simpleType>
|
||||
<xsd:simpleType name="ST_OLEType">
|
||||
<xsd:restriction base="xsd:string">
|
||||
<xsd:enumeration value="Embed"/>
|
||||
<xsd:enumeration value="Link"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
<xsd:simpleType name="ST_OLEDrawAspect">
|
||||
<xsd:restriction base="xsd:string">
|
||||
<xsd:enumeration value="Content"/>
|
||||
<xsd:enumeration value="Icon"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
<xsd:simpleType name="ST_OLEUpdateMode">
|
||||
<xsd:restriction base="xsd:string">
|
||||
<xsd:enumeration value="Always"/>
|
||||
<xsd:enumeration value="OnCall"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
<xsd:simpleType name="ST_FillType">
|
||||
<xsd:restriction base="xsd:string">
|
||||
<xsd:enumeration value="gradientCenter"/>
|
||||
<xsd:enumeration value="solid"/>
|
||||
<xsd:enumeration value="pattern"/>
|
||||
<xsd:enumeration value="tile"/>
|
||||
<xsd:enumeration value="frame"/>
|
||||
<xsd:enumeration value="gradientUnscaled"/>
|
||||
<xsd:enumeration value="gradientRadial"/>
|
||||
<xsd:enumeration value="gradient"/>
|
||||
<xsd:enumeration value="background"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
</xsd:schema>
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
|
||||
xmlns="urn:schemas-microsoft-com:office:powerpoint"
|
||||
targetNamespace="urn:schemas-microsoft-com:office:powerpoint" elementFormDefault="qualified"
|
||||
attributeFormDefault="unqualified">
|
||||
<xsd:element name="iscomment" type="CT_Empty"/>
|
||||
<xsd:element name="textdata" type="CT_Rel"/>
|
||||
<xsd:complexType name="CT_Empty"/>
|
||||
<xsd:complexType name="CT_Rel">
|
||||
<xsd:attribute name="id" type="xsd:string"/>
|
||||
</xsd:complexType>
|
||||
</xsd:schema>
|
||||
Loaded 100 of 253 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user