Compare commits

..

9 Commits

Author SHA1 Message Date
CJACK.
e4a4b0ac0b Merge pull request #290 from CJackHwang/dev
修复3.6.0docker构建问题
2026-04-23 20:35:18 +08:00
CJACK.
22e3f32c43 Merge pull request #289 from jacob-sheng/fix-webui-config-docker-build
Fix webui batch import template Docker build
2026-04-23 20:32:22 +08:00
阿钖
9a24b8dcc2 Fix webui config template Docker build 2026-04-23 12:28:01 +00:00
CJACK.
68ccbd3785 Merge pull request #287 from CJackHwang/main
Add Code of Conduct and improve security policy clarity
2026-04-23 19:43:17 +08:00
CJACK.
845fc1453e Bump version from 3.6.0 to 3.6.1 2026-04-23 19:41:08 +08:00
CJACK.
fe486d0078 Revise SECURITY.md for clarity and detail
Updated the security policy to clarify supported versions, reporting procedures, and vulnerability definitions.
2026-04-23 19:39:15 +08:00
CJACK.
d5c186b312 Add Contributor Covenant Code of Conduct 2026-04-23 19:22:47 +08:00
CJACK.
4cec942fff Merge pull request #286 from ouqiting/fix_chat_histroy
feat: 对话记录支持保存并展示 HISTORY 内容
2026-04-23 16:30:16 +08:00
ouqiting
9a404e75fc feat: 对话记录支持保存并展示 HISTORY 内容 2026-04-23 14:47:43 +08:00
11 changed files with 310 additions and 1 deletions

128
CODE_OF_CONDUCT.md Normal file
View File

@@ -0,0 +1,128 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
cjackhwang@qq.com.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.

View File

@@ -3,6 +3,7 @@ FROM node:24 AS webui-builder
WORKDIR /app/webui
COPY webui/package.json webui/package-lock.json ./
RUN npm ci
COPY config.example.json /app/config.example.json
COPY webui ./
RUN npm run build

65
SECURITY.md Normal file
View File

@@ -0,0 +1,65 @@
# Security Policy
## Supported Versions
**Only the latest version** receives security updates.
If you are using an older version, please upgrade to the latest release.
| Version | Supported |
| -------------- | ------------------ |
| latest | :white_check_mark: |
| < latest | :x: |
> **Why?** This project is maintained by a single developer. Keeping only one active version ensures fast response times and avoids legacy maintenance overhead.
## What is a Security Vulnerability?
A **security vulnerability** is a bug that can be exploited to compromise:
- Data confidentiality (e.g., leaking secrets, user data)
- Data integrity (e.g., unauthorized modification)
- System availability (e.g., remote crash, denial of service)
- Privilege escalation (e.g., normal user gains admin rights)
**Examples**: SQL injection, command injection, path traversal, authentication bypass, insecure deserialization, sensitive data exposure.
**What is NOT a security vulnerability?**
Regular bugs like crashes (without exploit potential), incorrect return values, performance issues, missing features, or documentation typos. Please report those via **GitHub Issues** publicly.
## Reporting a Vulnerability
If you believe you have found a security vulnerability, **please do NOT open a public issue**.
Instead, send an email to: **cjackhwang@qq.com**
Please include as much as possible:
- A clear description of the issue
- Steps to reproduce (code / input / environment)
- Potential impact (what could an attacker do?)
- Suggested fix (if any)
You can expect:
- **Initial response** within 3 business days (acknowledgment)
- **Confirmation or clarification** within 7 days
- **Fix or decision** within 14 days (depending on complexity)
## What to Expect After Reporting
| Outcome | What happens |
| ------------------ | ------------- |
| **Accepted** | I will develop a fix, release a patch version, and may credit you in the release notes (unless you prefer anonymity). |
| **Declined** | I will explain why (e.g., not a security issue, already fixed, out of scope, or requires a larger redesign). |
| **Need more info** | I will ask follow-up questions. If no response within 14 days, the report may be considered stale. |
## Disclosure Policy
- Vulnerabilities will be **fixed privately** and then released as a new version.
- After the fix is released, I will typically publish a short security advisory (via GitHub Security Advisories) without revealing exploit details.
- Public disclosure can be coordinated if you request it.
## Recognition
I appreciate security researchers who follow responsible disclosure. Contributors who report valid, previously unknown vulnerabilities may be acknowledged in the project's README or release notes (unless they prefer to stay anonymous).
---
*Thank you for helping keep this project safe!*

View File

@@ -1 +1 @@
3.6.0
3.6.1

View File

@@ -44,6 +44,7 @@ func startChatHistory(store *chathistory.Store, r *http.Request, a *auth.Request
Stream: stdReq.Stream,
UserInput: extractSingleUserInput(stdReq.Messages),
Messages: extractAllMessages(stdReq.Messages),
HistoryText: stdReq.HistoryText,
FinalPrompt: stdReq.FinalPrompt,
})
startParams := chathistory.StartParams{
@@ -53,6 +54,7 @@ func startChatHistory(store *chathistory.Store, r *http.Request, a *auth.Request
Stream: stdReq.Stream,
UserInput: extractSingleUserInput(stdReq.Messages),
Messages: extractAllMessages(stdReq.Messages),
HistoryText: stdReq.HistoryText,
FinalPrompt: stdReq.FinalPrompt,
}
session := &chatHistorySession{

View File

@@ -271,3 +271,56 @@ func TestChatCompletionsSkipsHistoryWhenDisabled(t *testing.T) {
t.Fatalf("expected disabled history to stay empty, got %#v", snapshot.Items)
}
}
func TestChatCompletionsHistorySplitPersistsHistoryText(t *testing.T) {
historyStore := newTestChatHistoryStore(t)
ds := &inlineUploadDSStub{}
h := &Handler{
Store: mockOpenAIConfig{
wideInput: true,
historySplitEnabled: true,
historySplitTurns: 1,
},
Auth: streamStatusAuthStub{},
DS: ds,
ChatHistory: historyStore,
}
reqBody := `{"model":"deepseek-chat","messages":[{"role":"system","content":"system instructions"},{"role":"user","content":"first user turn"},{"role":"assistant","content":"","reasoning_content":"hidden reasoning","tool_calls":[{"name":"search","arguments":{"query":"docs"}}]},{"role":"tool","name":"search","tool_call_id":"call-1","content":"tool result"},{"role":"user","content":"latest user turn"}],"stream":false}`
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(reqBody))
req.Header.Set("Authorization", "Bearer direct-token")
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
h.ChatCompletions(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected 200, got %d body=%s", rec.Code, rec.Body.String())
}
snapshot, err := historyStore.Snapshot()
if err != nil {
t.Fatalf("snapshot failed: %v", err)
}
if len(snapshot.Items) != 1 {
t.Fatalf("expected one history item, got %d", len(snapshot.Items))
}
full, err := historyStore.Get(snapshot.Items[0].ID)
if err != nil {
t.Fatalf("expected detail item, got %v", err)
}
if full.HistoryText == "" {
t.Fatalf("expected history text to be persisted")
}
if !strings.Contains(full.HistoryText, "first user turn") || !strings.Contains(full.HistoryText, "tool result") {
t.Fatalf("expected earlier turns in history text, got %q", full.HistoryText)
}
if strings.Contains(full.HistoryText, "latest user turn") {
t.Fatalf("expected latest turn to stay out of persisted history text, got %q", full.HistoryText)
}
if len(ds.uploadCalls) != 1 {
t.Fatalf("expected history upload to happen, got %d", len(ds.uploadCalls))
}
if full.HistoryText != string(ds.uploadCalls[0].Data) {
t.Fatalf("expected persisted history text to match uploaded HISTORY.txt contents")
}
}

View File

@@ -51,6 +51,7 @@ func (h *Handler) applyHistorySplit(ctx context.Context, a *auth.RequestAuth, st
}
stdReq.Messages = promptMessages
stdReq.HistoryText = historyText
stdReq.RefFileIDs = prependUniqueRefFileID(stdReq.RefFileIDs, fileID)
stdReq.FinalPrompt, stdReq.ToolNames = buildHistorySplitPrompt(promptMessages, reasoningContent, stdReq.ToolsRaw, stdReq.ToolChoice, stdReq.Thinking)
return stdReq, nil

View File

@@ -195,6 +195,37 @@ func TestApplyHistorySplitSkipsFirstTurn(t *testing.T) {
}
}
func TestApplyHistorySplitCarriesHistoryText(t *testing.T) {
ds := &inlineUploadDSStub{}
h := &Handler{
Store: mockOpenAIConfig{
wideInput: true,
historySplitEnabled: true,
historySplitTurns: 1,
},
DS: ds,
}
req := map[string]any{
"model": "deepseek-chat",
"messages": historySplitTestMessages(),
}
stdReq, err := normalizeOpenAIChatRequest(h.Store, req, "")
if err != nil {
t.Fatalf("normalize failed: %v", err)
}
out, err := h.applyHistorySplit(context.Background(), &auth.RequestAuth{DeepSeekToken: "token"}, stdReq)
if err != nil {
t.Fatalf("apply history split failed: %v", err)
}
if len(ds.uploadCalls) != 1 {
t.Fatalf("expected 1 upload call, got %d", len(ds.uploadCalls))
}
if out.HistoryText != string(ds.uploadCalls[0].Data) {
t.Fatalf("expected history text to be preserved on normalized request")
}
}
func TestChatCompletionsHistorySplitUploadsHistoryAndKeepsLatestPrompt(t *testing.T) {
ds := &inlineUploadDSStub{}
h := &Handler{

View File

@@ -46,6 +46,7 @@ type Entry struct {
Stream bool `json:"stream"`
UserInput string `json:"user_input,omitempty"`
Messages []Message `json:"messages,omitempty"`
HistoryText string `json:"history_text,omitempty"`
FinalPrompt string `json:"final_prompt,omitempty"`
ReasoningContent string `json:"reasoning_content,omitempty"`
Content string `json:"content,omitempty"`
@@ -94,6 +95,7 @@ type StartParams struct {
Stream bool
UserInput string
Messages []Message
HistoryText string
FinalPrompt string
}
@@ -244,6 +246,7 @@ func (s *Store) Start(params StartParams) (Entry, error) {
Stream: params.Stream,
UserInput: strings.TrimSpace(params.UserInput),
Messages: cloneMessages(params.Messages),
HistoryText: params.HistoryText,
FinalPrompt: strings.TrimSpace(params.FinalPrompt),
}
s.details[entry.ID] = entry

View File

@@ -8,6 +8,7 @@ type StandardRequest struct {
ResolvedModel string
ResponseModel string
Messages []any
HistoryText string
ToolsRaw any
FinalPrompt string
ToolNames []string

View File

@@ -181,11 +181,35 @@ function MergedPromptView({ item, t }) {
)
}
function HistoryTextView({ item, t }) {
const historyText = (item?.history_text || '').trim()
if (!historyText) return null
return (
<div className="max-w-4xl mx-auto rounded-2xl border border-border bg-background px-5 py-4">
<div className="text-[11px] uppercase tracking-[0.12em] text-muted-foreground mb-3 text-left">
HISTORY
</div>
<div className="text-sm leading-7 text-foreground whitespace-pre-wrap break-words font-mono">
<ExpandableText
text={historyText}
threshold={Math.floor(MESSAGE_COLLAPSE_AT / 4)}
expandLabel={t('chatHistory.expand')}
collapseLabel={t('chatHistory.collapse')}
buttonClassName="text-foreground hover:text-muted-foreground"
/>
</div>
</div>
)
}
function DetailConversation({ selectedItem, t, viewMode, detailScrollRef, assistantStartRef, bottomButtonClassName }) {
if (!selectedItem) return null
return (
<>
<HistoryTextView item={selectedItem} t={t} />
{viewMode === 'list'
? <RequestMessages item={selectedItem} t={t} />
: <MergedPromptView item={selectedItem} t={t} />}