Compare commits
3 Commits
cd1b541427
...
cd70b50180
| Author | SHA1 | Date | |
|---|---|---|---|
| cd70b50180 | |||
| 96390df254 | |||
| 371d4ce327 |
@@ -239,9 +239,18 @@ router.post(
|
||||
validateBody(saveFileSchema),
|
||||
asyncHandler(async (req: Request, res: Response) => {
|
||||
const { path: relPath, content } = req.body
|
||||
console.log('[save] relPath:', relPath)
|
||||
console.log('[save] content length:', content?.length)
|
||||
console.log('[save] content preview:', content?.slice(0, 100))
|
||||
const { fullPath } = resolveNotebookPath(relPath)
|
||||
console.log('[save] fullPath:', fullPath)
|
||||
await fs.mkdir(path.dirname(fullPath), { recursive: true })
|
||||
console.log('[save] before writeFile')
|
||||
await fs.writeFile(fullPath, content, 'utf-8')
|
||||
console.log('[save] after writeFile, checking file...')
|
||||
const writtenContent = await fs.readFile(fullPath, 'utf-8')
|
||||
console.log('[save] written content length:', writtenContent?.length)
|
||||
console.log('[save] written content preview:', writtenContent?.slice(0, 100))
|
||||
|
||||
successResponse(res, null)
|
||||
}),
|
||||
|
||||
@@ -183,7 +183,7 @@ class SessionPersistenceService implements SessionPersistence {
|
||||
try {
|
||||
const content = await fs.readFile(filePath, 'utf-8')
|
||||
const data = JSON.parse(content)
|
||||
data.activeDays = Object.values(data.days).filter(d => d.totalDuration > 0).length
|
||||
data.activeDays = Object.values(data.days).filter((d: any) => d.totalDuration > 0).length
|
||||
return data
|
||||
} catch (err) {
|
||||
return createEmptyMonthData(year, month)
|
||||
@@ -195,7 +195,7 @@ class SessionPersistenceService implements SessionPersistence {
|
||||
try {
|
||||
const content = await fs.readFile(filePath, 'utf-8')
|
||||
const data = JSON.parse(content)
|
||||
data.totalActiveDays = Object.values(data.months).filter(m => m.totalDuration > 0).length
|
||||
data.totalActiveDays = Object.values(data.months).filter((m: any) => m.totalDuration > 0).length
|
||||
return data
|
||||
} catch (err) {
|
||||
return createEmptyYearData(year)
|
||||
|
||||
@@ -389,7 +389,7 @@ class TimeTrackerService {
|
||||
const yearData = await this.persistence.getYearData(targetYear)
|
||||
totalDuration = yearData.yearlyTotal
|
||||
activeDays = Object.values(yearData.months).reduce((sum, m) => {
|
||||
return sum + Object.entries(m).filter(([_, d]) => (d as { totalDuration: number }).totalDuration > 0).length
|
||||
return sum + Object.entries(m).filter(([_, d]) => (d as any).totalDuration > 0).length
|
||||
}, 0)
|
||||
|
||||
for (const [month, summary] of Object.entries(yearData.months)) {
|
||||
|
||||
99
remote/xcopencodeweb/README.md
Normal file
99
remote/xcopencodeweb/README.md
Normal file
@@ -0,0 +1,99 @@
|
||||
# XCOpenCodeWeb
|
||||
|
||||
XCOpenCodeWeb 是一个基于 Web 的 AI 编程助手界面,用于与 OpenCode 服务器交互。
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 使用单文件 exe(推荐)
|
||||
|
||||
直接下载 `XCOpenCodeWeb.exe`,双击运行:
|
||||
|
||||
```bash
|
||||
# 默认端口 3000
|
||||
XCOpenCodeWeb.exe
|
||||
|
||||
# 指定端口
|
||||
XCOpenCodeWeb.exe --port 8080
|
||||
|
||||
# 查看帮助
|
||||
XCOpenCodeWeb.exe --help
|
||||
```
|
||||
|
||||
启动后访问 http://localhost:3000
|
||||
|
||||
### 从源码运行
|
||||
|
||||
需要安装 [Bun](https://bun.sh) 或 Node.js 20+
|
||||
|
||||
```bash
|
||||
# 安装依赖
|
||||
bun install
|
||||
|
||||
# 构建前端
|
||||
bun run build
|
||||
|
||||
# 启动服务器
|
||||
bun server/index.js --port 3000
|
||||
```
|
||||
|
||||
## 构建单文件 exe
|
||||
|
||||
```bash
|
||||
cd web
|
||||
bun run build:exe
|
||||
```
|
||||
|
||||
输出:`web/XCOpenCodeWeb.exe`(约 320MB)
|
||||
|
||||
详细文档:[docs/single-file-exe-build.md](docs/single-file-exe-build.md)
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
├── ui/ # 前端组件库
|
||||
├── web/
|
||||
│ ├── src/ # 前端源码
|
||||
│ ├── server/ # 后端服务器
|
||||
│ ├── bin/ # CLI 工具
|
||||
│ └── dist/ # 构建输出
|
||||
├── docs/ # 文档
|
||||
└── AGENTS.md # AI Agent 参考文档
|
||||
```
|
||||
|
||||
## 常用命令
|
||||
|
||||
```bash
|
||||
# 开发模式
|
||||
bun run dev # 前端热更新
|
||||
bun run dev:server # 启动开发服务器
|
||||
|
||||
# 构建
|
||||
bun run build # 构建前端
|
||||
bun run build:exe # 构建单文件 exe
|
||||
|
||||
# 代码检查
|
||||
bun run type-check:web # TypeScript 类型检查
|
||||
bun run lint:web # ESLint 检查
|
||||
```
|
||||
|
||||
## 依赖
|
||||
|
||||
- [Bun](https://bun.sh) - 运行时和打包工具
|
||||
- [React](https://react.dev) - 前端框架
|
||||
- [Express](https://expressjs.com) - 后端服务器
|
||||
- [Tailwind CSS](https://tailwindcss.com) - 样式框架
|
||||
|
||||
## 配置
|
||||
|
||||
### 环境变量
|
||||
|
||||
| 变量 | 说明 |
|
||||
|------|------|
|
||||
| `XCOpenCodeWeb_PORT` | 服务器端口(默认 3000) |
|
||||
| `OPENCODE_HOST` | 外部 OpenCode 服务器地址 |
|
||||
| `OPENCODE_PORT` | 外部 OpenCode 端口 |
|
||||
| `OPENCODE_SKIP_START` | 跳过启动 OpenCode |
|
||||
|
||||
## 许可证
|
||||
|
||||
MIT
|
||||
99
services/xcopencodeweb/README.md
Normal file
99
services/xcopencodeweb/README.md
Normal file
@@ -0,0 +1,99 @@
|
||||
# XCOpenCodeWeb
|
||||
|
||||
XCOpenCodeWeb 是一个基于 Web 的 AI 编程助手界面,用于与 OpenCode 服务器交互。
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 使用单文件 exe(推荐)
|
||||
|
||||
直接下载 `XCOpenCodeWeb.exe`,双击运行:
|
||||
|
||||
```bash
|
||||
# 默认端口 3000
|
||||
XCOpenCodeWeb.exe
|
||||
|
||||
# 指定端口
|
||||
XCOpenCodeWeb.exe --port 8080
|
||||
|
||||
# 查看帮助
|
||||
XCOpenCodeWeb.exe --help
|
||||
```
|
||||
|
||||
启动后访问 http://localhost:3000
|
||||
|
||||
### 从源码运行
|
||||
|
||||
需要安装 [Bun](https://bun.sh) 或 Node.js 20+
|
||||
|
||||
```bash
|
||||
# 安装依赖
|
||||
bun install
|
||||
|
||||
# 构建前端
|
||||
bun run build
|
||||
|
||||
# 启动服务器
|
||||
bun server/index.js --port 3000
|
||||
```
|
||||
|
||||
## 构建单文件 exe
|
||||
|
||||
```bash
|
||||
cd web
|
||||
bun run build:exe
|
||||
```
|
||||
|
||||
输出:`web/XCOpenCodeWeb.exe`(约 320MB)
|
||||
|
||||
详细文档:[docs/single-file-exe-build.md](docs/single-file-exe-build.md)
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
├── ui/ # 前端组件库
|
||||
├── web/
|
||||
│ ├── src/ # 前端源码
|
||||
│ ├── server/ # 后端服务器
|
||||
│ ├── bin/ # CLI 工具
|
||||
│ └── dist/ # 构建输出
|
||||
├── docs/ # 文档
|
||||
└── AGENTS.md # AI Agent 参考文档
|
||||
```
|
||||
|
||||
## 常用命令
|
||||
|
||||
```bash
|
||||
# 开发模式
|
||||
bun run dev # 前端热更新
|
||||
bun run dev:server # 启动开发服务器
|
||||
|
||||
# 构建
|
||||
bun run build # 构建前端
|
||||
bun run build:exe # 构建单文件 exe
|
||||
|
||||
# 代码检查
|
||||
bun run type-check:web # TypeScript 类型检查
|
||||
bun run lint:web # ESLint 检查
|
||||
```
|
||||
|
||||
## 依赖
|
||||
|
||||
- [Bun](https://bun.sh) - 运行时和打包工具
|
||||
- [React](https://react.dev) - 前端框架
|
||||
- [Express](https://expressjs.com) - 后端服务器
|
||||
- [Tailwind CSS](https://tailwindcss.com) - 样式框架
|
||||
|
||||
## 配置
|
||||
|
||||
### 环境变量
|
||||
|
||||
| 变量 | 说明 |
|
||||
|------|------|
|
||||
| `XCOpenCodeWeb_PORT` | 服务器端口(默认 3000) |
|
||||
| `OPENCODE_HOST` | 外部 OpenCode 服务器地址 |
|
||||
| `OPENCODE_PORT` | 外部 OpenCode 端口 |
|
||||
| `OPENCODE_SKIP_START` | 跳过启动 OpenCode |
|
||||
|
||||
## 许可证
|
||||
|
||||
MIT
|
||||
BIN
services/xcopencodeweb/XCOpenCodeWeb.exe
Normal file
BIN
services/xcopencodeweb/XCOpenCodeWeb.exe
Normal file
Binary file not shown.
12
shared/modules/opencode/index.ts
Normal file
12
shared/modules/opencode/index.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { defineModule } from '../types.js'
|
||||
|
||||
export const OPENCODE_MODULE = defineModule({
|
||||
id: 'opencode',
|
||||
name: 'OpenCode',
|
||||
basePath: '/opencode',
|
||||
order: 15,
|
||||
version: '1.0.0',
|
||||
backend: {
|
||||
enabled: false,
|
||||
},
|
||||
})
|
||||
@@ -85,6 +85,8 @@ export const useMarkdownLogic = ({
|
||||
const readOnlyRef = useRef(readOnly)
|
||||
const ctxRef = useRef<Ctx | null>(null)
|
||||
|
||||
const lastContentRef = useRef<string>('')
|
||||
|
||||
useEffect(() => {
|
||||
onChangeRef.current = onChange
|
||||
}, [])
|
||||
@@ -117,23 +119,28 @@ export const useMarkdownLogic = ({
|
||||
}
|
||||
}, [readOnly])
|
||||
|
||||
// 在只读模式下动态更新内容
|
||||
// 在只读模式下动态更新内容(仅当 content 真正变化时)
|
||||
useEffect(() => {
|
||||
if (!ctxRef.current || !readOnly) return
|
||||
|
||||
// 只有当 content 真正变化时才更新编辑器
|
||||
if (content === lastContentRef.current) return
|
||||
lastContentRef.current = content
|
||||
|
||||
try {
|
||||
const view = ctxRef.current.get(editorViewCtx)
|
||||
const parser = ctxRef.current.get(parserCtx)
|
||||
const doc = parser(content)
|
||||
if (!doc) return
|
||||
|
||||
const state = view.state
|
||||
view.dispatch(state.tr.replaceWith(0, state.doc.content.size, doc))
|
||||
view.dispatch(view.state.tr.replaceWith(0, view.state.doc.content.size, doc))
|
||||
} catch {
|
||||
// 编辑器可能尚未就绪
|
||||
}
|
||||
}, [content, readOnly])
|
||||
|
||||
return useEditor((root) => {
|
||||
lastContentRef.current = content
|
||||
return Editor.make()
|
||||
.config((ctx) => {
|
||||
ctxRef.current = ctx
|
||||
|
||||
@@ -1,62 +1,7 @@
|
||||
import { useRef, useEffect, useState } from 'react'
|
||||
import React from 'react'
|
||||
|
||||
export const HomePage = () => {
|
||||
const webviewRef = useRef<HTMLWebViewElement>(null)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const startOpencodeServer = async () => {
|
||||
try {
|
||||
const result = await window.electronAPI?.opencodeStartServer()
|
||||
if (!result?.success) {
|
||||
setError(result?.error || 'Failed to start opencode server')
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Unknown error')
|
||||
}
|
||||
}
|
||||
|
||||
startOpencodeServer()
|
||||
|
||||
return () => {
|
||||
window.electronAPI?.opencodeStopServer()
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const webview = webviewRef.current
|
||||
if (!webview) return
|
||||
|
||||
webview.addEventListener('did-fail-load', (e) => {
|
||||
console.error('[HomePage] Failed to load:', e)
|
||||
setIsLoading(false)
|
||||
})
|
||||
|
||||
webview.addEventListener('did-finish-load', () => {
|
||||
setIsLoading(false)
|
||||
})
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="h-full w-full relative">
|
||||
{isLoading && (
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-white dark:bg-gray-900 z-10">
|
||||
<div className="text-gray-500 dark:text-gray-400">正在启动 opencode 服务...</div>
|
||||
</div>
|
||||
)}
|
||||
{error && (
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-white dark:bg-gray-900 z-10">
|
||||
<div className="text-red-500">启动失败: {error}</div>
|
||||
</div>
|
||||
)}
|
||||
<webview
|
||||
ref={webviewRef}
|
||||
src="http://127.0.0.1:4096"
|
||||
className="w-full h-full"
|
||||
nodeintegration={false}
|
||||
webpreferences="contextIsolation=yes"
|
||||
/>
|
||||
</div>
|
||||
<div className="h-full w-full" />
|
||||
)
|
||||
}
|
||||
|
||||
11
src/modules/opencode/OpenCodePage.tsx
Normal file
11
src/modules/opencode/OpenCodePage.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import React from 'react'
|
||||
|
||||
export const OpenCodePage: React.FC = () => {
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto w-full px-14 py-12 pb-40">
|
||||
<h1 className="text-2xl font-bold text-gray-800 dark:text-gray-200 mb-8 flex items-center gap-2">
|
||||
OpenCode
|
||||
</h1>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
11
src/modules/opencode/index.tsx
Normal file
11
src/modules/opencode/index.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Code } from 'lucide-react'
|
||||
import { OpenCodePage } from './OpenCodePage'
|
||||
import { OPENCODE_MODULE } from '@shared/modules/opencode'
|
||||
import { createFrontendModule } from '@/lib/module-registry'
|
||||
|
||||
export default createFrontendModule(OPENCODE_MODULE, {
|
||||
icon: Code,
|
||||
component: OpenCodePage,
|
||||
})
|
||||
|
||||
export { OpenCodePage }
|
||||
@@ -331,7 +331,9 @@ export const useTabStore = create<TabStore>((set, get) => {
|
||||
const isAsyncImportProcessing = isAsyncImportProcessingContent(tab.content)
|
||||
if (isAsyncImportProcessing) return
|
||||
|
||||
await saveFileContent(filePath, tab.unsavedContent)
|
||||
const contentToSave = tab.unsavedContent
|
||||
|
||||
await saveFileContent(filePath, contentToSave)
|
||||
|
||||
set((state) => {
|
||||
const newTabs = new Map(state.tabs)
|
||||
@@ -339,7 +341,8 @@ export const useTabStore = create<TabStore>((set, get) => {
|
||||
if (currentTab) {
|
||||
newTabs.set(filePath, {
|
||||
...currentTab,
|
||||
content: currentTab.unsavedContent,
|
||||
content: contentToSave,
|
||||
unsavedContent: contentToSave,
|
||||
isEditing: false,
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user